diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index fb6efa505..000000000 --- a/.claude/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "permissions": {} -} diff --git a/tools/check_guide.py b/.github/scripts/check_guide.py similarity index 98% rename from tools/check_guide.py rename to .github/scripts/check_guide.py index b74217e38..69c2eff74 100644 --- a/tools/check_guide.py +++ b/.github/scripts/check_guide.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Checks the bilingual GitHub Pages guide in docs/guide/. - python3 tools/check_guide.py [guide-dir] + python3 .github/scripts/check_guide.py [guide-dir] Fails (exit 1) when: 1. an HTML file does not parse into balanced elements; @@ -153,7 +153,7 @@ def check(guide): def main(): here = os.path.dirname(os.path.abspath(__file__)) - guide = sys.argv[1] if len(sys.argv) > 1 else os.path.join(here, "..", "docs", "guide") + guide = sys.argv[1] if len(sys.argv) > 1 else os.path.join(here, "..", "..", "docs", "guide") failures = check(guide) for f in failures: print("FAIL", f) diff --git a/.github/scripts/ft-mobile/build-android.sh b/.github/scripts/ft-mobile/build-android.sh new file mode 100755 index 000000000..cc5ced80d --- /dev/null +++ b/.github/scripts/ft-mobile/build-android.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Build free-threaded CPython (--disable-gil) for aarch64-linux-android with +# CPython's own tooling (Platforms/Android). Usage: build-android.sh [VERSION] +# Needs ANDROID_HOME (SDK with cmdline-tools/latest) and a JDK 17, 21 or 25. +set -euo pipefail +here="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +source "$here/common.sh" + +PY_VERSION="${1:-3.15.0}" +HOST=aarch64-linux-android +: "${ANDROID_HOME:?ANDROID_HOME must point at the Android SDK}" + +src="$(fetch_source "$PY_VERSION")" +cd "$src" + +# `Platforms/Android package` lists the files to ship with `git ls-files`, so +# the unpacked tarball needs an index that tracks Platforms/Android. +if [ ! -d .git ]; then + git init -q . + git add Platforms/Android +fi + +# Install the NDK version the tooling pins (android-env.sh would do it on first +# use; doing it here makes a missing cmdline-tools fail early and visibly). +ndk_version="$(sed -n 's/^ndk_version=//p' Platforms/Android/android-env.sh)" +[ -n "$ndk_version" ] || die "no ndk_version in Platforms/Android/android-env.sh" +log "NDK required by CPython $PY_VERSION: $ndk_version" +if [ ! -e "$ANDROID_HOME/ndk/$ndk_version/package.xml" ]; then + yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;$ndk_version" >/dev/null || true +fi +[ -e "$ANDROID_HOME/ndk/$ndk_version/package.xml" ] || die "NDK $ndk_version not installed" + +# --disable-gil goes to configure after `--`, for the build Python and the host. +python3 Platforms/Android configure-build -- --disable-gil +python3 Platforms/Android make-build +python3 Platforms/Android pythoninfo-build +python3 Platforms/Android configure-host "$HOST" -- --disable-gil +python3 Platforms/Android make-host "$HOST" +python3 Platforms/Android package "$HOST" + +# ---- verification ------------------------------------------------------- +minor="$(py_minor "$PY_VERSION")" +archive="$(ls cross-build/$HOST/dist/python-*-$HOST.tar.gz)" +log "package: $archive" + +# The build Python must itself be free-threaded. +cross-build/build/python -c 'import sys, sysconfig; assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1; print("build python free-threaded:", sys.version)' + +check="$FT_WORK/android-check" +rm -rf "$check"; mkdir -p "$check" +tar -xzf "$archive" -C "$check" + +require_gil_disabled "$check/prefix/include/python${minor}t/pyconfig.h" +lib="$(ls "$check"/prefix/lib/libpython${minor}t*.so* 2>/dev/null | head -n 1 || true)" +[ -n "$lib" ] || die "no libpython${minor}t*.so in the package" +log "ok: $lib" +file "$lib" | tee /dev/stderr | grep -q 'ARM aarch64' || die "$lib is not an aarch64 ELF" +[ -d "$check/prefix/lib/python${minor}t" ] || die "no lib/python${minor}t in the package" + +cp "$archive" "$FT_OUT/" +echo "Android archive:" >> "${GITHUB_STEP_SUMMARY:-/dev/null}" +record_sha256 "$FT_OUT/$(basename "$archive")" diff --git a/.github/scripts/ft-mobile/build-ios.sh b/.github/scripts/ft-mobile/build-ios.sh new file mode 100755 index 000000000..f4e16966e --- /dev/null +++ b/.github/scripts/ft-mobile/build-ios.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Build free-threaded CPython (--disable-gil) as an iOS XCframework with +# CPython's own tooling (Platforms/Apple). Usage: build-ios.sh [VERSION] +# Needs macOS with a full Xcode and python3 >= 3.10. +set -euo pipefail +here="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +source "$here/common.sh" + +PY_VERSION="${1:-3.15.0}" +# The XCframework needs every slice Platforms/Apple declares: one device host +# and two simulator hosts (arm64, x86_64). +HOSTS=(arm64-apple-ios arm64-apple-ios-simulator x86_64-apple-ios-simulator) + +src="$(fetch_source "$PY_VERSION")" +cd "$src" +xcodebuild -version + +# Platforms/Apple names the standard library directory "python" +# when it packages and in the Xcode build helper, but a free-threaded build +# installs "pythont" (LIBDEST has $(ABI_THREAD)). Without these two +# edits `package` fails copying the xcprivacy files and the helper script would +# not find the standard library. Each edit is asserted so a changed upstream +# script fails loudly instead of silently skipping the patch. +python3 -I - <<'PY' +import pathlib +def patch(path, old, new): + p = pathlib.Path(path) + s = p.read_text() + assert s.count(old) == 1, f"{path}: expected exactly one {old!r}" + p.write_text(s.replace(old, new)) +patch("Platforms/Apple/__main__.py", + 'version_tag = ".".join(version.split(".")[:2])', + 'version_tag = ".".join(version.split(".")[:2]) + "t"') +patch("Platforms/Apple/testbed/Python.xcframework/build/utils.sh", + 'grep -E "^python3\\.\\d+$"', + 'grep -E "^python3\\.[0-9]+t?$"') +PY + +# --disable-gil goes to configure after `--`, for the build Python and each host. +python3 Platforms/Apple configure-build -- --disable-gil +python3 Platforms/Apple make-build +python3 Platforms/Apple pythoninfo-build +for host in "${HOSTS[@]}"; do + python3 Platforms/Apple configure-host iOS "$host" -- --disable-gil + python3 Platforms/Apple make-host iOS "$host" +done +python3 Platforms/Apple package iOS + +# ---- verification ------------------------------------------------------- +minor="$(py_minor "$PY_VERSION")" +archive="$(ls cross-build/dist/python-*-iOS-XCframework.tar.gz)" +log "package: $archive" + +# On macOS the build interpreter is python.exe (cross-build/build/python is a directory). +build_py=cross-build/build/python.exe; [ -x "$build_py" ] || build_py=cross-build/build/python +"$build_py" -c 'import sys, sysconfig; assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1; print("build python free-threaded:", sys.version)' + +check="$FT_WORK/ios-check" +rm -rf "$check"; mkdir -p "$check" +tar -xzf "$archive" -C "$check" +xcf="$check/Python.xcframework" +[ -d "$xcf" ] || die "no Python.xcframework in the package" + +[ -d "$xcf/lib/python${minor}t" ] || die "no lib/python${minor}t in the XCframework" +slices=0 +for slice in "$xcf"/ios-*; do + slices=$((slices + 1)) + # Headers/pyconfig.h is a wrapper that includes pyconfig-.h. + found=0 + for h in "$slice"/Python.framework/Headers/pyconfig-*.h; do + require_gil_disabled "$h"; found=$((found + 1)) + done + [ "$found" -gt 0 ] || die "no pyconfig-.h in $slice" + [ -e "$slice/lib/libpython${minor}t.dylib" ] || die "no libpython${minor}t.dylib in $slice" + lipo -info "$slice/Python.framework/Python" +done +[ "$slices" -eq 2 ] || die "expected 2 slices (device, simulator), found $slices" + +cp "$archive" "$FT_OUT/" +echo "iOS archive:" >> "${GITHUB_STEP_SUMMARY:-/dev/null}" +record_sha256 "$FT_OUT/$(basename "$archive")" diff --git a/.github/scripts/ft-mobile/common.sh b/.github/scripts/ft-mobile/common.sh new file mode 100755 index 000000000..74ddd4035 --- /dev/null +++ b/.github/scripts/ft-mobile/common.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Helpers shared by build-android.sh and build-ios.sh. Source this file, do not run it. + +# FT_WORK: scratch directory (source tree and cross-build output). +# FT_OUT : directory that receives the archives the workflow uploads. +FT_WORK="${FT_WORK:-$PWD/.tmp/ft-mobile}" +FT_OUT="${FT_OUT:-$PWD/.tmp/ft-mobile-out}" + +log() { echo "[ft-mobile] $*" >&2; } +die() { echo "[ft-mobile] ERROR: $*" >&2; exit 1; } + +# "3.15.0" -> "3.15" (also "3.15.0rc1" -> "3.15"). +py_minor() { echo "$1" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/'; } + +# Download and unpack the CPython source tarball; print the source directory. +# CPython's mobile tooling needs the directory to be a git work tree only for +# `Platforms/Android package` (it runs `git ls-files` there), so the caller +# decides whether to `git init`. +fetch_source() { + local version="$1" + local dirver="${version%%[a-z]*}" # the ftp directory has no rc/alpha suffix + local tgz="$FT_WORK/Python-$version.tgz" + mkdir -p "$FT_WORK" "$FT_OUT" + if [ ! -f "$tgz" ]; then + log "downloading CPython $version" + curl -fL --retry 5 --retry-all-errors -o "$tgz" \ + "https://www.python.org/ftp/python/$dirver/Python-$version.tgz" + fi + if [ ! -d "$FT_WORK/Python-$version" ]; then + tar -xzf "$tgz" -C "$FT_WORK" + fi + echo "$FT_WORK/Python-$version" +} + +# Fail unless FILE contains a line matching `#define Py_GIL_DISABLED 1`. +require_gil_disabled() { + local file="$1" + grep -Eq '^#define Py_GIL_DISABLED 1$' "$file" \ + || die "$file does not define Py_GIL_DISABLED 1" + log "ok: Py_GIL_DISABLED 1 in $file" +} + +# Print "sha256 name" lines for the given files, to stdout and the job summary. +record_sha256() { + local f line + for f in "$@"; do + line="$(cd "$(dirname "$f")" && shasum -a 256 "$(basename "$f")")" + echo "$line" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + echo "$line" >> "$GITHUB_STEP_SUMMARY" + fi + done +} diff --git a/tools/release/README.md b/.github/scripts/release/README.md similarity index 81% rename from tools/release/README.md rename to .github/scripts/release/README.md index a4f6302d9..cfb4de357 100644 --- a/tools/release/README.md +++ b/.github/scripts/release/README.md @@ -7,7 +7,7 @@ between runs, and any existing `release` (e.g. a stale one from an older workflo ignored and overwritten. 1. Every push to `develop` triggers `.github/workflows/release-sync.yml`, which runs - `tools/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly + `.github/scripts/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly one commit (`Release: sync from develop `) whose parent is that develop commit and whose tree is the develop tree minus the files below. This happens even when nothing is dropped. It is a no-op only if `release` already has that tree and that parent. @@ -31,10 +31,10 @@ Only Markdown is ever dropped. ## Local use - tools/release/sync-release.sh --dry-run # list paths that would be dropped - tools/release/sync-release.sh # regenerate local release branch - tools/release/sync-release.sh --push # also push to origin - tools/release/test-sync-release.sh # tests (throwaway repo in .tmp/) + .github/scripts/release/sync-release.sh --dry-run # list paths that would be dropped + .github/scripts/release/sync-release.sh # regenerate local release branch + .github/scripts/release/sync-release.sh --push # also push to origin + .github/scripts/release/test-sync-release.sh # tests (throwaway repo in .tmp/) The script uses plumbing and a private index, so your working tree, index and HEAD are untouched. It refuses to run while `release` is checked out. diff --git a/tools/release/sync-release.sh b/.github/scripts/release/sync-release.sh similarity index 100% rename from tools/release/sync-release.sh rename to .github/scripts/release/sync-release.sh diff --git a/tools/release/test-sync-release.sh b/.github/scripts/release/test-sync-release.sh similarity index 99% rename from tools/release/test-sync-release.sh rename to .github/scripts/release/test-sync-release.sh index cba1d3139..2ee99e3f9 100755 --- a/tools/release/test-sync-release.sh +++ b/.github/scripts/release/test-sync-release.sh @@ -2,7 +2,7 @@ # Tests for sync-release.sh. Builds a throwaway repo under /.tmp/. set -u HERE="$(cd "$(dirname "$0")" && pwd)" -ROOT="$(cd "$HERE/../.." && pwd)" +ROOT="$(cd "$HERE/../../.." && pwd)" SYNC="$HERE/sync-release.sh" SCRATCH="$ROOT/.tmp/release-sync-test.$$" FAILS=0; PASSES=0 diff --git a/tools/stubs/check-stubs.sh b/.github/scripts/stubs/check-stubs.sh similarity index 55% rename from tools/stubs/check-stubs.sh rename to .github/scripts/stubs/check-stubs.sh index b8c1c41d3..5e74fa0f8 100755 --- a/tools/stubs/check-stubs.sh +++ b/.github/scripts/stubs/check-stubs.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash -# Type-checks tools/stubs/consumer.py against a directory of generated Kotlin-name stubs (issue #31). +# Type-checks .github/scripts/stubs/consumer.py against a directory of generated Kotlin-name stubs (issue #31). # -# tools/stubs/check-stubs.sh [venv-dir] +# .github/scripts/stubs/check-stubs.sh [venv-dir] # # is what `generatePythonStubs` wrote, e.g. -# ksp-fixtures/compose/build/generated/pythonStubs/desktopMain +# python-multiplatform-compose/fixtures/compose/build/generated/pythonStubs/desktopMain # The venv defaults to /.tmp/stubs-venv (git-ignored) and is created on first use. set -euo pipefail -root="$(cd "$(dirname "$0")/../.." && pwd)" +root="$(cd "$(dirname "$0")/../../.." && pwd)" stubs="$(cd "${1:?usage: check-stubs.sh [venv-dir]}" && pwd)" venv="${2:-$root/.tmp/stubs-venv}" @@ -17,10 +17,10 @@ if [ ! -x "$venv/bin/mypy" ]; then "$venv/bin/pip" install --quiet mypy fi -# tools/stubs/mypy.ini: `strict`, and `warn_unused_ignores` for the consumer so that the lines it +# .github/scripts/stubs/mypy.ini: `strict`, and `warn_unused_ignores` for the consumer so that the lines it # expects to be rejected fail the check when they stop being. Errors inside the stubs themselves are # reported too: MYPYPATH modules are checked, not silenced. MYPYPATH="$stubs" "$venv/bin/mypy" \ - --config-file "$root/tools/stubs/mypy.ini" \ + --config-file "$root/.github/scripts/stubs/mypy.ini" \ --cache-dir "$root/.tmp/mypy-cache" \ - "$root/tools/stubs/consumer.py" + "$root/.github/scripts/stubs/consumer.py" diff --git a/tools/stubs/consumer.py b/.github/scripts/stubs/consumer.py similarity index 50% rename from tools/stubs/consumer.py rename to .github/scripts/stubs/consumer.py index 3111280c6..99e4e61d8 100644 --- a/tools/stubs/consumer.py +++ b/.github/scripts/stubs/consumer.py @@ -1,15 +1,29 @@ -"""A small consumer of the generated Kotlin-name stubs, type-checked by `check-stubs.sh` (issue #31). +"""A small consumer of the generated Kotlin-named stubs, type-checked by `check-stubs.sh` (issue #31). -It is the code a user writes against the binder -- Kotlin names, Kotlin parameter names -- and it -must type-check against the stubs generated for the real Compose jars: a Modifier chain started from -the class and continued from an instance, `Checkbox`, `Text`, and a container with its trailing -`content` lambda. The last four lines are **meant** to be rejected; they carry `# type: ignore[...]` -and mypy runs with `warn_unused_ignores`, so a stub that stopped rejecting one fails the check too. +It is the code a user writes against the binder, and it must type-check against the stubs generated +for the real Compose jars: a Modifier chain started from the class and continued from an instance, +`Checkbox`, `Text`, and a container with its trailing `content` lambda. + +Names: a declaration is reachable by its Kotlin name and by its Pythonic (snake_case) alias, and the +stubs carry both (issue #131), so both spellings are written below. Keywords: a stub can name a +parameter only once, and it names it the way `inspect.signature` does -- by the Pythonic keyword +(`on_checked_change`). The Kotlin keyword (`onCheckedChange=`) still works at run time; a checker +reports it, so it is not written here. + +The last four lines are **meant** to be rejected; they carry `# type: ignore[...]` and mypy runs +with `warn_unused_ignores`, so a stub that stopped rejecting one fails the check too. """ from typing import assert_type -from androidx.compose.foundation.layout import Arrangement, Column, Row, fillMaxWidth, padding__Dp +from androidx.compose.foundation.layout import ( + Arrangement, + Column, + Row, + fill_max_width, + fillMaxWidth, + padding__Dp, +) from androidx.compose.material.icons import Icons from androidx.compose.material3 import Checkbox, Icon, Text from androidx.compose.ui import Modifier @@ -17,13 +31,18 @@ # A chain started from the class, the way Kotlin writes `Modifier.padding(16.dp)`. chain: Modifier = Modifier.fillMaxWidth().padding(16.0).size(24.0) -# ... and continued from an instance, and with Kotlin parameter names. +# ... and continued from an instance, with keyword arguments. more: Modifier = chain.padding(horizontal=8.0, vertical=4.0).padding(start=1.0, bottom=2.0) # The explicit table-key spelling, as an attribute and as a module function. explicit: Modifier = chain.padding__Dp(4.0) by_function: Modifier = padding__Dp(chain, 4.0) by_extension: Modifier = fillMaxWidth(chain, fraction=0.5) +# Issue #131: the Pythonic alias of a module function and of a method is the same declaration. +by_alias: Modifier = fill_max_width(chain, fraction=0.5) +assert_type(fill_max_width(chain), Modifier) +pythonic_chain: Modifier = Modifier.fill_max_width().padding(16.0).fill_max_width(0.5) + checked = True @@ -33,7 +52,7 @@ def on_change(value: bool) -> None: Checkbox(checked, on_change, modifier=more, enabled=True) -Checkbox(checked=False, onCheckedChange=None) +Checkbox(checked=False, on_checked_change=None) Text("hello", modifier=Modifier.padding(8.0)) @@ -44,18 +63,18 @@ def content() -> None: Column(modifier=chain, content=lambda scope: None) Row(content=lambda scope: None) # `SpaceBetween` is a `HorizontalOrVertical`: a `Vertical` slot and a `Horizontal` slot both take it (#71). -Column(verticalArrangement=Arrangement.SpaceBetween, content=lambda scope: None) -Row(horizontalArrangement=Arrangement.SpaceBetween, content=lambda scope: None) +Column(vertical_arrangement=Arrangement.SpaceBetween, content=lambda scope: None) +Row(horizontal_arrangement=Arrangement.SpaceBetween, content=lambda scope: None) # An extension property on a type nested in an object (issue #68): `Icons.Default` is `Icons.Filled`. # `assert_type`, not an annotation: an `Any` (the stub before #53/#67) would satisfy an annotation. assert_type(Icons.Default.Add, ImageVector) -Icon(Icons.Default.Add, contentDescription=None) +Icon(Icons.Default.Add, content_description=None) # Rejected on purpose. (`padding("sixteen")` is not among them: one `padding` overload takes a # `PaddingValues`, a type the stubs cannot name -- `PaddingValues(...)` the factory owns the name -- so # it is `Any` and accepts anything.) -Modifier.fillMaxWidth("wide") # type: ignore[arg-type] +Modifier.fill_max_width("wide") # type: ignore[arg-type] Checkbox("not a bool", on_change) # type: ignore[call-overload] Checkbox(checked, on_change, enabled="yes") # type: ignore[call-overload] Column(chain) # type: ignore[call-arg] diff --git a/tools/stubs/mypy.ini b/.github/scripts/stubs/mypy.ini similarity index 100% rename from tools/stubs/mypy.ini rename to .github/scripts/stubs/mypy.ini diff --git a/.github/workflows/android-native.yml b/.github/workflows/android-native.yml index 6ff4ff985..3e77c4f17 100644 --- a/.github/workflows/android-native.yml +++ b/.github/workflows/android-native.yml @@ -1,15 +1,16 @@ name: Android Native CI on: + # Checked on develop after merging, not on every pull request (user decision, + # 2026-10-17). A red develop is fixed forward by its own pull request. push: branches: [ "develop", "main" ] - pull_request: - branches: [ "develop", "main" ] - + workflow_dispatch: concurrency: - # A newer push to the same ref supersedes this run; frees the shared macOS runners. + # A run in progress finishes; a newer push replaces only the queued run (frequent merges + # to develop otherwise cancelled every run before it reported). group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: compile: diff --git a/.github/workflows/build-ft-mobile.yml b/.github/workflows/build-ft-mobile.yml new file mode 100644 index 000000000..0b9d05954 --- /dev/null +++ b/.github/workflows/build-ft-mobile.yml @@ -0,0 +1,80 @@ +name: Build free-threaded CPython for mobile + +# Builds CPython with --disable-gil ("3.15t") for Android arm64 and iOS with +# CPython's own tooling (Platforms/Android, Platforms/Apple). Manual only: the +# builds take a long time and the archives are inputs to later work (#163). +# See docs/platforms/free-threaded-mobile.md. + +on: + # Runs on develop when the build itself changes (workflow_dispatch needs the file on the + # default branch, main, which develop reaches only through a release). + push: + branches: [develop] + paths: + - ".github/workflows/build-ft-mobile.yml" + - ".github/scripts/ft-mobile/**" + workflow_dispatch: + inputs: + python_version: + description: CPython version to build (a python.org source release) + default: "3.15.0" + required: true + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + android: + name: Android aarch64 (free-threaded) + runs-on: ubuntu-24.04 + timeout-minutes: 120 + env: + FT_WORK: ${{ github.workspace }}/.tmp/ft-mobile + FT_OUT: ${{ github.workspace }}/.tmp/ft-mobile-out + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Install build Python prerequisites + run: > + sudo apt-get update && sudo apt-get install -y + build-essential pkg-config libssl-dev zlib1g-dev libffi-dev + libbz2-dev liblzma-dev libsqlite3-dev libzstd-dev + + - name: Build + run: .github/scripts/ft-mobile/build-android.sh "${{ inputs.python_version || '3.15.0' }}" + + - uses: actions/upload-artifact@v4 + with: + name: python-${{ inputs.python_version || '3.15.0' }}t-android-aarch64 + path: ${{ env.FT_OUT }}/*.tar.gz + if-no-files-found: error + + ios: + name: iOS XCframework (free-threaded) + runs-on: macos-15 + timeout-minutes: 180 + env: + FT_WORK: ${{ github.workspace }}/.tmp/ft-mobile + FT_OUT: ${{ github.workspace }}/.tmp/ft-mobile-out + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build + run: .github/scripts/ft-mobile/build-ios.sh "${{ inputs.python_version || '3.15.0' }}" + + - uses: actions/upload-artifact@v4 + with: + name: python-${{ inputs.python_version || '3.15.0' }}t-ios-xcframework + path: ${{ env.FT_OUT }}/*.tar.gz + if-no-files-found: error diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index 67c9c2c7f..0b43ff3e5 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -1,15 +1,16 @@ name: Desktop CI on: + # Checked on develop after merging, not on every pull request (user decision, + # 2026-10-17). A red develop is fixed forward by its own pull request. push: branches: [ "develop", "main" ] - pull_request: - branches: [ "develop", "main" ] - + workflow_dispatch: concurrency: - # A newer push to the same ref supersedes this run; frees the shared macOS runners. + # A run in progress finishes; a newer push replaces only the queued run (frequent merges + # to develop otherwise cancelled every run before it reported). group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: test: @@ -41,6 +42,191 @@ jobs: restore-keys: | ${{ runner.os }}-konan- + # The dev agent's protocol (SPEC L-12, #233): stdlib-only, so the runner's own python3 runs it. + - name: Dev agent protocol tests + working-directory: python-multiplatform/src/test/python + run: python3 -m unittest -v test_devagent + - name: Run Desktop Tests # Using --console=plain so logs are readable in GitHub Actions run: ./gradlew :python-multiplatform:desktopTest --console=plain + + # The dev agent's protocol on the other desktop OSes (#238). Its socket options differ per platform + # (SO_EXCLUSIVEADDRUSE on Windows, SO_REUSEADDR on POSIX), and the port-takeover and restart tests are + # what check them. Stdlib only: no JDK, no Gradle. + dev-agent: + name: Dev agent protocol (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Dev agent protocol tests + working-directory: python-multiplatform/src/test/python + run: python -m unittest -v test_devagent + + # pythonx-compose's UI.ipynb, end to end (#26, #213): the installed pythonx-compose wheel drives a + # real desktop Compose host, pixel-compared with the same composables drawn from Kotlin. Its own + # job and Gradle invocation, so it neither hides nor is hidden by the module tests above. + notebook-e2e: + name: Notebook E2E (pythonx-compose wheel) + runs-on: macos-latest + env: + WHEEL_DIR: ${{ github.workspace }}/.tmp/pythonx-compose-dist + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Beside this repository, as on a developer machine; the wheel is built from it, not installed + # from PyPI, so the scenarios follow pythonx-compose develop. + - name: Checkout pythonx-compose develop + uses: actions/checkout@v4 + with: + repository: thisisthepy/pythonx-compose + ref: develop + path: .tmp/pythonx-compose + fetch-depth: 1 + + - name: Setup uv + uses: astral-sh/setup-uv@v6 + + - name: Build the pythonx-compose wheel + run: | + uv build --wheel --directory .tmp/pythonx-compose --out-dir "$WHEEL_DIR" + ls -l "$WHEEL_DIR" + git -C .tmp/pythonx-compose log -1 --format='pythonx-compose %h %s' + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + cache: 'gradle' + + - name: Cache Konan + uses: actions/cache@v4 + with: + path: ~/.konan + key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-konan- + + # No skip property: without a wheel every scenario fails by name, which is what CI wants. + - name: Run the notebook scenarios + run: > + ./gradlew :python-multiplatform-compose:fixtures:notebook-e2e:desktopTest --rerun + -PpythonxComposeWheel="$WHEEL_DIR" --console=plain + + - name: Count the scenarios + if: always() + run: | + python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" + import glob, xml.etree.ElementTree as ET + files = glob.glob("python-multiplatform-compose/fixtures/notebook-e2e/build/test-results/desktopTest/*.xml") + total = failed = skipped = 0 + for f in files: + suite = ET.parse(f).getroot() + total += int(suite.get("tests", 0)) + failed += int(suite.get("failures", 0)) + int(suite.get("errors", 0)) + skipped += int(suite.get("skipped", 0)) + print(f"Notebook E2E: {total} tests, {total - failed - skipped} passed, {failed} failed, {skipped} skipped ({len(files)} suites)") + PY + cat "$GITHUB_STEP_SUMMARY" + + - name: Upload the test reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: notebook-e2e-reports + path: | + python-multiplatform-compose/fixtures/notebook-e2e/build/reports/tests/desktopTest + python-multiplatform-compose/fixtures/notebook-e2e/build/test-results/desktopTest + + # The notebook E2E in a GraalVM native image on macOS (#249, #251). It takes `ui`, `ui-text` and + # `foundation` from compose-multiplatform-core-extended, which a runner reads from the fork's GitHub + # Packages registry; until the repository variable COMPOSE_EXTENDED_VERSION names a published + # development version (`1.11.1-ext.N-dev.`) the job is skipped. + notebook-e2e-native-image: + name: Notebook E2E in a native image (macOS) + if: ${{ vars.COMPOSE_EXTENDED_VERSION != '' }} + runs-on: macos-latest + permissions: + contents: read + packages: read + env: + WHEEL_DIR: ${{ github.workspace }}/.tmp/pythonx-compose-dist + COMPOSE_EXTENDED_VERSION: ${{ vars.COMPOSE_EXTENDED_VERSION }} + COMPOSE_EXTENDED_GROUP_PREFIX: ${{ vars.COMPOSE_EXTENDED_GROUP_PREFIX || 'org.thisisthepy.compose' }} + COMPOSE_EXTENDED_REPOSITORY: ${{ vars.COMPOSE_EXTENDED_REPOSITORY || 'https://maven.pkg.github.com/thisisthepy/compose-multiplatform-core-extended' }} + # GitHub Packages asks for a token even for a public package. A read:packages PAT in the secret + # GH_PACKAGES_TOKEN is used when it exists; otherwise this repository's own token. + GITHUB_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN || secrets.GITHUB_TOKEN }} + TOKEN_SOURCE: ${{ secrets.GH_PACKAGES_TOKEN != '' && 'GH_PACKAGES_TOKEN' || 'GITHUB_TOKEN' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Checkout pythonx-compose develop + uses: actions/checkout@v4 + with: + repository: thisisthepy/pythonx-compose + ref: develop + path: .tmp/pythonx-compose + fetch-depth: 1 + + # Before any build: can this token read the fork's packages? One known POM, so a refusal is + # reported as what it is rather than as a dependency resolution error minutes later. + - name: Check access to the fork's packages + run: | + group_path="${COMPOSE_EXTENDED_GROUP_PREFIX//.//}/ui" + pom="$COMPOSE_EXTENDED_REPOSITORY/$group_path/ui-desktop/$COMPOSE_EXTENDED_VERSION/ui-desktop-$COMPOSE_EXTENDED_VERSION.pom" + status=$(curl -s -o /dev/null -w '%{http_code}' -u "${GITHUB_ACTOR}:${GITHUB_TOKEN}" "$pom") + echo "reading $pom with $TOKEN_SOURCE: HTTP $status" + if [ "$status" != "200" ]; then + echo "::error::The fork's packages could not be read with $TOKEN_SOURCE (HTTP $status at $pom). Add a personal access token (classic) with read:packages as the repository secret GH_PACKAGES_TOKEN, or check COMPOSE_EXTENDED_VERSION / COMPOSE_EXTENDED_REPOSITORY." + exit 1 + fi + + - name: Setup uv + uses: astral-sh/setup-uv@v6 + + - name: Build the pythonx-compose wheel + run: uv build --wheel --directory .tmp/pythonx-compose --out-dir "$WHEEL_DIR" + + - name: Setup Java 21 (Gradle) + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + cache: 'gradle' + + # The JDK 25 toolchain native-image comes from, found by Gradle's toolchain resolution. + - name: Setup Liberica NIK 25 + uses: graalvm/setup-graalvm@v1 + with: + java-version: '25' + distribution: 'liberica' + set-java-home: 'false' + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Records Compose and skiko's configuration against the fork version this run uses, then builds + # the image and runs it with JAVA_HOME and PYTHONHOME removed: 20 of 20 and no libjvm. + - name: Build and run the native image + run: > + ./gradlew :python-multiplatform-compose:fixtures:notebook-e2e:notebookE2eNativeAgent + :python-multiplatform-compose:fixtures:notebook-e2e:notebookE2eNativeRun + -PpythonxComposeWheel="$WHEEL_DIR" + -PcomposeExtended.version="$COMPOSE_EXTENDED_VERSION" + -PcomposeExtended.groupPrefix="$COMPOSE_EXTENDED_GROUP_PREFIX" + -PcomposeExtended.mavenLocal=false + -PcomposeExtended.repository="$COMPOSE_EXTENDED_REPOSITORY" + --console=plain diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 0bd46c48c..f1287532b 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -1,15 +1,16 @@ name: iOS CI on: + # Checked on develop after merging, not on every pull request (user decision, + # 2026-10-17). A red develop is fixed forward by its own pull request. push: branches: [ "develop", "main" ] - pull_request: - branches: [ "develop", "main" ] - + workflow_dispatch: concurrency: - # A newer push to the same ref supersedes this run; frees the shared macOS runners. + # A run in progress finishes; a newer push replaces only the queued run (frequent merges + # to develop otherwise cancelled every run before it reported). group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: compile: @@ -38,3 +39,55 @@ jobs: - name: Compile iOS Simulator Arm64 # Compile iOS Simulator which includes commonMain + nativeMain + iosMain run: ./gradlew :python-multiplatform:compileKotlinIosSimulatorArm64 --console=plain + + # The klib walker on Kotlin/Native (#234, #237, #240): object handles, their lifetime and member + # functions, called from Python on the iOS simulator. Its own job and Gradle invocation. + klib-fixture: + name: Klib fixture on the iOS simulator + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + cache: 'gradle' + + - name: Cache Konan + uses: actions/cache@v4 + with: + path: ~/.konan + key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-konan- + + - name: Run the klib fixture's suite + run: ./gradlew :python-multiplatform-gradle-plugin-fixtures:klib-artifact:iosSimulatorArm64Test --rerun --console=plain + + - name: Count the tests + if: always() + run: | + python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" + import glob, xml.etree.ElementTree as ET + files = glob.glob("python-multiplatform-gradle-plugin/fixtures/klib-artifact/build/test-results/iosSimulatorArm64Test/*.xml") + total = failed = skipped = 0 + for f in files: + suite = ET.parse(f).getroot() + total += int(suite.get("tests", 0)) + failed += int(suite.get("failures", 0)) + int(suite.get("errors", 0)) + skipped += int(suite.get("skipped", 0)) + print(f"Klib fixture (iOS simulator): {total} tests, {total - failed - skipped} passed, {failed} failed, {skipped} skipped ({len(files)} suites)") + PY + cat "$GITHUB_STEP_SUMMARY" + + - name: Upload the test reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: klib-fixture-ios-reports + path: | + python-multiplatform-gradle-plugin/fixtures/klib-artifact/build/reports/tests/iosSimulatorArm64Test + python-multiplatform-gradle-plugin/fixtures/klib-artifact/build/test-results/iosSimulatorArm64Test diff --git a/.github/workflows/release-sync.yml b/.github/workflows/release-sync.yml index 746a0b9de..8914610b3 100644 --- a/.github/workflows/release-sync.yml +++ b/.github/workflows/release-sync.yml @@ -29,7 +29,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" # Not used as a base (release is regenerated); the script reads the # remote sha itself for --force-with-lease. - bash tools/release/sync-release.sh --source "${GITHUB_SHA}" --push + bash .github/scripts/release/sync-release.sh --source "${GITHUB_SHA}" --push - name: Open or update PR release -> main env: @@ -43,7 +43,7 @@ jobs: echo "release already equals main; nothing to PR"; exit 0 fi title="Release: merge release into main" - body="Automated sync of \`develop\` (${GITHUB_SHA::7}) into \`release\`. Markdown outside README.md and docs/ subfolders is removed by tools/release/sync-release.sh." + body="Automated sync of \`develop\` (${GITHUB_SHA::7}) into \`release\`. Markdown outside README.md and docs/ subfolders is removed by .github/scripts/release/sync-release.sh." existing="$(gh pr list --head release --base main --state open --json number --jq '.[0].number // empty')" if [ -n "$existing" ]; then gh pr edit "$existing" --title "$title" --body "$body" diff --git a/.github/workflows/stubs.yml b/.github/workflows/stubs.yml index b6a7a785d..2d9356bba 100644 --- a/.github/workflows/stubs.yml +++ b/.github/workflows/stubs.yml @@ -5,21 +5,15 @@ name: Kotlin-name stubs # build resolves (`compose-plugin` in gradle/libs.versions.toml) and publishes the result. # # - every push to develop and every pull request: the stubs are generated, type-checked against -# tools/stubs/consumer.py with mypy (issue #31), and uploaded as the workflow artifact `kotlin-stubs`; +# .github/scripts/stubs/consumer.py with mypy (issue #31), and uploaded as the workflow artifact `kotlin-stubs`; # - every `v*` tag: the same stubs are also attached to the tag's GitHub release as `kotlin-stubs.zip`. on: + # Checked on develop after merging, not on every pull request (user decision, + # 2026-10-17). A red develop is fixed forward by its own pull request. push: branches: [ "develop" ] tags: [ "v*" ] - pull_request: - branches: [ "develop" ] - paths: - - "python-multiplatform-gradle-plugin/**" - - "ksp-fixtures/compose/**" - - "tools/stubs/**" - - "gradle/libs.versions.toml" - - ".github/workflows/stubs.yml" workflow_dispatch: permissions: @@ -27,7 +21,7 @@ permissions: concurrency: group: stubs-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: stubs: @@ -52,10 +46,10 @@ jobs: python-version: '3.13' - name: Generate the stubs over the Compose jars - run: ./gradlew :ksp-fixtures:compose:generatePythonStubs --console=plain + run: ./gradlew :python-multiplatform-compose:fixtures:compose:generatePythonStubs --console=plain - name: Type-check a consumer against them (mypy) - run: tools/stubs/check-stubs.sh ksp-fixtures/compose/build/generated/pythonStubs/desktopMain + run: .github/scripts/stubs/check-stubs.sh python-multiplatform-compose/fixtures/compose/build/generated/pythonStubs/desktopMain - name: Assemble the artifact run: | @@ -65,7 +59,7 @@ jobs: out=build/kotlin-stubs rm -rf "$out" mkdir -p "$out" - cp -R ksp-fixtures/compose/build/generated/pythonStubs/desktopMain/. "$out/" + cp -R python-multiplatform-compose/fixtures/compose/build/generated/pythonStubs/desktopMain/. "$out/" cat > "$out/README.md" <\`, or the IDE's "stub path" / interpreter paths). It is checked with mypy against - \`tools/stubs/consumer.py\` in the commit above. + \`.github/scripts/stubs/consumer.py\` in the commit above. README (cd build && zip -qr kotlin-stubs.zip kotlin-stubs) @@ -129,11 +123,11 @@ jobs: python-version: '3.13' - name: Generate the stubs over the Compose jars - run: ./gradlew :ksp-fixtures:compose:generatePythonStubs --console=plain + run: ./gradlew :python-multiplatform-compose:fixtures:compose:generatePythonStubs --console=plain - name: No two stub paths are equal under case folding run: | - python3 - ksp-fixtures/compose/build/generated/pythonStubs/desktopMain <<'PY' + python3 - python-multiplatform-compose/fixtures/compose/build/generated/pythonStubs/desktopMain <<'PY' import os, sys seen = {} for root, dirs, files in os.walk(sys.argv[1]): @@ -146,4 +140,4 @@ jobs: PY - name: Type-check a consumer against them (mypy) - run: tools/stubs/check-stubs.sh ksp-fixtures/compose/build/generated/pythonStubs/desktopMain + run: .github/scripts/stubs/check-stubs.sh python-multiplatform-compose/fixtures/compose/build/generated/pythonStubs/desktopMain diff --git a/.github/workflows/typedpython.yml b/.github/workflows/typedpython.yml index c03f0ea4d..4c47c2ac8 100644 --- a/.github/workflows/typedpython.yml +++ b/.github/workflows/typedpython.yml @@ -3,17 +3,17 @@ name: TypedPython CI # The TypedPython gate and compiler are Python (python-multiplatform-ksp/src/{main,test}/python). # Gradle does not run them; this workflow does. on: + # Checked on develop after merging, not on every pull request (user decision, + # 2026-10-17). A red develop is fixed forward by its own pull request. push: branches: [ "develop", "main" ] - paths: [ "python-multiplatform-ksp/**", "ksp-fixtures/app/**", ".github/workflows/typedpython.yml" ] - pull_request: - branches: [ "develop", "main" ] - paths: [ "python-multiplatform-ksp/**", "ksp-fixtures/app/**", ".github/workflows/typedpython.yml" ] - + paths: [ "python-multiplatform-ksp/**", "python-multiplatform-ksp/fixtures/app/**", ".github/workflows/typedpython.yml" ] + workflow_dispatch: concurrency: - # A newer push to the same ref supersedes this run; frees the shared macOS runners. + # A run in progress finishes; a newer push replaces only the queued run (frequent merges + # to develop otherwise cancelled every run before it reported). group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: test: @@ -34,11 +34,11 @@ jobs: - name: Create the environment # pyrefly is pinned exactly: the gate reads its versioned type-report format. run: | - uv venv .venv --python 3.13 - uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest + uv venv .venv --python 3.15t + uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest numpy - name: Behaviour tests - run: .venv/bin/python -m pytest -q -m "not measurement" + run: .venv/bin/python -m pytest -q -m "not measurement and not race_mutant" - name: Measurements (recorded, sanity bounds only) # A shared runner is not an idle machine (AGENTS.md §8): read ratios, not absolute times. @@ -62,8 +62,8 @@ jobs: - name: Create the environment run: | - uv venv .venv --python 3.13 - uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest + uv venv .venv --python 3.15t + uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest numpy - name: Relax ASLR entropy (ASan shadow mapping on newer kernels) run: sudo sysctl -w vm.mmap_rnd_bits=28 || true @@ -78,15 +78,15 @@ jobs: # only (tools/sanitizers.py env prints it); the gate's pyrefly runs without it. run: | env $(.venv/bin/python tools/sanitizers.py env) \ - .venv/bin/python -m pytest -q -m "not measurement" \ - src/test/python/test_cgen.py src/test/python/test_runtime.py src/test/python/test_pipeline.py + .venv/bin/python -m pytest -q -m "not measurement and not race_mutant" \ + src/test/python/test_cgen.py src/test/python/test_runtime.py src/test/python/test_pipeline.py src/test/python/test_interval_proofs.py app-fixture: # #45: a TypedPython-compiled module calling Kotlin through the upcall table, in the app # fixture's desktop JVM. buildTypedPythonBridge needs a host Python with pyrefly; without one the # four TypedPythonKotlinCallTest tests skip. Locally that is allowed; here a skip fails the job. # macOS only: elsewhere the JVM does not load libpython with global symbols, so the - # dynamic_lookup extension cannot resolve the C API (ksp-fixtures/app/build.gradle.kts). + # dynamic_lookup extension cannot resolve the C API (python-multiplatform-ksp/fixtures/app/build.gradle.kts). name: TypedPython from Kotlin, app fixture (macos-latest) runs-on: macos-latest steps: @@ -103,20 +103,20 @@ jobs: - name: Create the TypedPython host environment working-directory: python-multiplatform-ksp run: | - uv venv .venv --python 3.13 - uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest + uv venv .venv --python 3.15t + uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest numpy - name: App fixture tests run: | - rm -rf ksp-fixtures/app/build/test-results/desktopTest - ./gradlew :ksp-fixtures:app:desktopTest --rerun --console=plain \ + rm -rf python-multiplatform-ksp/fixtures/app/build/test-results/desktopTest + ./gradlew :python-multiplatform-ksp:fixtures:app:desktopTest --rerun --console=plain \ -PtypedpythonPython="$PWD/python-multiplatform-ksp/.venv/bin/python" - name: The TypedPython tests ran (none skipped) run: | python3 - <<'PY' import glob, sys, xml.etree.ElementTree as ET - cases = [c for p in glob.glob("ksp-fixtures/app/build/test-results/desktopTest/*.xml") + cases = [c for p in glob.glob("python-multiplatform-ksp/fixtures/app/build/test-results/desktopTest/*.xml") for c in ET.parse(p).getroot().iter("testcase") if c.get("classname") == "fixture.app.TypedPythonKotlinCallTest"] skipped = [c.get("name") for c in cases if c.find("skipped") is not None] diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index a800e5dcd..291124f33 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -13,15 +13,16 @@ name: WasmJS CI # `test` runs the real suite, and runs only when there is a runtime to run it against. on: + # Checked on develop after merging, not on every pull request (user decision, + # 2026-10-17). A red develop is fixed forward by its own pull request. push: branches: [ "develop", "main" ] - pull_request: - branches: [ "develop", "main" ] - + workflow_dispatch: concurrency: - # A newer push to the same ref supersedes this run; frees the shared macOS runners. + # A run in progress finishes; a newer push replaces only the queued run (frequent merges + # to develop otherwise cancelled every run before it reported). group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: compile: @@ -129,7 +130,7 @@ jobs: # `-PrequireWasmRuntime` a missing wheel is a failure, not a skip, so fetch them first: by URL, # each accepted only against the sha256 the script pins. - name: Fetch the pinned wasm wheels - run: bash tools/wasm/build-cpython.sh wheels + run: bash python-multiplatform/scripts/wasm/build-cpython.sh wheels # `-PrequireWasmRuntime` is the second half of the guard, and it is what makes this job unable # to repeat the original defect: if either task would skip -- no interpreter, or no diff --git a/.gitignore b/.gitignore index 670457d97..70924b2d7 100644 --- a/.gitignore +++ b/.gitignore @@ -49,8 +49,11 @@ google-services.json # subagent worktrees live inside the repo .worktrees/ -# repo-local caches: emsdk, the CPython wasm build and its staged runtime (tools/wasm/build-cpython.sh) +# repo-local caches: emsdk, the CPython wasm build and its staged runtime (python-multiplatform/scripts/wasm/build-cpython.sh) .caches/ # scratch space for agents and scripts; delete when done .tmp/ + +# Local Claude Code settings: per machine, not part of the repository. +.claude/ diff --git a/LICENSE b/LICENSE index 958f105ed..57bc88a15 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -MIT License - -Copyright (c) 2024 thisisthepy - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.md b/README.md index b4a7106f8..500f6086e 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ English | [한국어](docs/locale/README_ko.md) # 🐍 python-multiplatform -**Real CPython inside Kotlin Multiplatform — Kotlin calls Python, Python calls Kotlin, on every target.** +**Real CPython inside Kotlin Multiplatform, Kotlin calls Python, Python calls Kotlin, on every target.** -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) [![Kotlin Multiplatform](https://img.shields.io/badge/Kotlin-Multiplatform-7F52FF.svg?logo=kotlin&logoColor=white)](https://kotlinlang.org/docs/multiplatform.html) [![CPython](https://img.shields.io/badge/CPython-3.14-3776AB.svg?logo=python&logoColor=white)](https://www.python.org/) [![Platforms](https://img.shields.io/badge/platforms-JVM%20%7C%20Android%20%7C%20iOS%20%7C%20Android%20native%20%7C%20wasm%20(exp.)-lightgrey.svg)](#-platforms) @@ -21,31 +21,31 @@ English | [한국어](docs/locale/README_ko.md) ## 💡 Why -Python has the libraries; Kotlin Multiplatform has the reach — desktop, Android, iOS, the web. +Python has the libraries; Kotlin Multiplatform has the reach, desktop, Android, iOS, the web. python-multiplatform puts **the real CPython interpreter** inside a Kotlin Multiplatform app and builds a two-way bridge across it: - Kotlin gets a **typed, Kotlin-idiomatic object model** over Python: a `PyList` *is* a `MutableList`, a `PyDict` *is* a `MutableMap`, a Python error *is* a `Throwable`. - Python gets **Kotlin classes as ordinary Python**: `Greeter('Kotlin').greet(2)`, `await` on a - `suspend fun`, Kotlin extension functions as methods — all through a table generated at build time, + `suspend fun`, Kotlin extension functions as methods, all through a table generated at build time, so it works where reflection does not: Kotlin/Native and GraalVM native images. It is CPython itself, not a re-implementation, so C extensions keep working. ## ✨ Features -- 🔌 **Downcalls** — the CPython Stable ABI (~330 functions) as one `expect` surface with an `actual` +- 🔌 **Downcalls**: the CPython Stable ABI (~330 functions) as one `expect` surface with an `actual` per platform: Panama `invokeExact` on desktop, `RegisterNatives` JNI on Android, cinterop on Native. -- 🧩 **Object model** — `PyObject`, `PyType`, `PyException`, basic types, `PyList` / `PyDict` / +- 🧩 **Object model**: `PyObject`, `PyType`, `PyException`, basic types, `PyList` / `PyDict` / `PySet` / `PyTuple` implementing Kotlin collection interfaces, modules, callables, conversion. -- 🚀 **Upcalls** — a KSP processor generates a function table; Python constructs Kotlin classes, calls +- 🚀 **Upcalls**: a KSP processor generates a function table; Python constructs Kotlin classes, calls methods, reads and writes properties, and `await`s `suspend` functions with cancellation. -- 📦 **Bind prebuilt libraries** — the Gradle plugin walks jars (Compose included) and generates the +- 📦 **Bind prebuilt libraries**: the Gradle plugin walks jars (Compose included) and generates the bindings and `.pyi` stubs, with default-argument omission and value-class support. -- ♻️ **No manual memory management** — wrappers release their Python reference when collected; reference +- ♻️ **No manual memory management**: wrappers release their Python reference when collected; reference cycles that cross the boundary are collected by Python's cyclic GC. -- 🧪 **Measured** — every boundary mechanism ships with overhead tests; the cost table is rendered from +- 🧪 **Measured**: every boundary mechanism ships with overhead tests; the cost table is rendered from real runs ([cost table](docs/investigations/cost-table.md)). ## 🚀 Quick start @@ -118,7 +118,7 @@ name, unchanged. ### Platform setup
-Desktop — apply the Gradle plugin so PYTHONHOME has a stdlib +Desktop, apply the Gradle plugin so PYTHONHOME has a stdlib ```kotlin plugins { @@ -138,7 +138,7 @@ prefix with `-Dpython.multiplatform.home=...`. See `docs/platforms/desktop-packa
-Android — call PythonBootstrap.initialize +Android, call PythonBootstrap.initialize ```kotlin import python.multiplatform.env.PythonBootstrap @@ -172,7 +172,7 @@ flowchart LR T --> K ``` -Behind each platform's single entry point, **every platform uses the same generated table** — there is +Behind each platform's single entry point, **every platform uses the same generated table**, there is no per-platform Python → Kotlin binding code. Details: [design notes](docs/design/). ## 🌍 Platforms @@ -188,7 +188,7 @@ no per-platform Python → Kotlin binding code. Details: [design notes](docs/des ## 📊 Status -Honest summary — see the [roadmap](docs/roadmap/ROADMAP.md) for what is left to do. +Honest summary, see the [roadmap](docs/roadmap/ROADMAP.md) for what is left to do. | Area | State | |---|---| @@ -213,10 +213,10 @@ Honest summary — see the [roadmap](docs/roadmap/ROADMAP.md) for what is left t ## 📖 Documentation -- 🌐 **[Guide](https://thisisthepy.github.io/python-multiplatform/)** (English / 한국어) — source in [`docs/guide/`](docs/guide/) -- 🏗 [Design notes](docs/design/) — architecture, downcalls, upcalls, lifetime, threading, binding policy -- 📱 [Platform notes](docs/platforms/) — Android FFM, wasm, CPython acquisition, GraalVM verification -- 🔬 [Investigations](docs/investigations/) — measurements and audits +- 🌐 **[Guide](https://thisisthepy.github.io/python-multiplatform/)** (English / 한국어), source in [`docs/guide/`](docs/guide/) +- 🏗 [Design notes](docs/design/), architecture, downcalls, upcalls, lifetime, threading, binding policy +- 📱 [Platform notes](docs/platforms/), Android FFM, wasm, CPython acquisition, GraalVM verification +- 🔬 [Investigations](docs/investigations/), measurements and audits - 🗺 [Roadmap](docs/roadmap/ROADMAP.md) - 🇰🇷 [README in Korean](docs/locale/README_ko.md) @@ -229,4 +229,4 @@ before opening a pull request. Building iOS targets requires macOS with Xcode. ## 📄 License -[MIT](LICENSE) © 2024 thisisthepy +[Apache-2.0](LICENSE) © 2024 thisisthepy diff --git a/benchmarks/cost_table.py b/benchmarks/cost_table.py index a854c36c4..c62da078e 100644 --- a/benchmarks/cost_table.py +++ b/benchmarks/cost_table.py @@ -749,7 +749,7 @@ def _samples(runs: list[dict[str, Any]], target: str, source: str, key: str, def _fmt_range(values: list[float], unit: str) -> str: if not values: - return "—" + return ", " lo, hi = min(values), max(values) suffix = "x" if unit == "x" else " ns" if abs(hi - lo) < 0.005: @@ -780,7 +780,7 @@ def render_markdown(report: dict[str, Any]) -> str: targets = report.get("targetsRequested") or DEFAULT_TARGETS lines: list[str] = [] - dirty = " **(working tree dirty — these numbers are not reproducible from the commit alone)**" \ + dirty = " **(working tree dirty, these numbers are not reproducible from the commit alone)**" \ if report.get("workingTreeDirty") else "" lines.append(f"_Generated by `benchmarks/cost_table.py` from " f"`{report.get('sourceFile', 'the newest JSON')}`._") @@ -815,23 +815,23 @@ def render_markdown(report: dict[str, Any]) -> str: host = report["host"] warm = report.get("benchmarkTestWarmup", {}) lines += [ - f"- **commit** `{report['commit'][:12]}` — {report.get('commitSubject', '')}{dirty}", + f"- **commit** `{report['commit'][:12]}`, {report.get('commitSubject', '')}{dirty}", f"- **branch** `{report.get('branch', '?')}`, worktree `{report.get('worktree', '?')}`", f"- **captured** {report['capturedAt']}" - + (" — the first of several cuts; each one's own time is in the source table below" + + (", the first of several cuts; each one's own time is in the source table below" if len(report.get("sources") or []) > 1 else ""), f"- **host** {host.get('cpu', '?')}, {host.get('cores', '?')} cores, " f"{host.get('os', '?')} {host.get('machine', '')}", f"- **load average** {report.get('loadAtStart')} at start, {report.get('loadAtEnd')} at end " f"(ceiling {report['loadCeiling']}, guard `{report.get('loadGuard')}`)" - + (" — of the first cut only; per-cut figures below" + + (", of the first cut only; per-cut figures below" if len(report.get("sources") or []) > 1 else ""), - f"- **runs per target** {report['runsRequested']} — ranges below are min–max over them", + f"- **runs per target** {report['runsRequested']}, ranges below are min–max over them", f"- **`UpcallBoundaryCostTest`** warmup and iteration counts are self-reported per target " "in the table below (the test prints them)", f"- **`BenchmarkTest`** warmup, read from the source at this commit: " f"`WARMUP` = {warm.get('WARMUP', '?')}, `WARMUP_BULK` = {warm.get('WARMUP_BULK', '?')} " - "(the ≥4096-char string rows use the second — see that file's own caveat)", + "(the ≥4096-char string rows use the second, see that file's own caveat)", "- **suite scope** full task, never `--tests`-filtered: narrowing the suite changes what is " "being measured", "", @@ -839,7 +839,7 @@ def render_markdown(report: dict[str, Any]) -> str: sources = report.get("sources") or [] if len(sources) > 1: - lines += ["This table is merged from several cuts — which target came from which:", ""] + lines += ["This table is merged from several cuts, which target came from which:", ""] lines += _table( ["cut", "commit", "captured", "load at start", "contaminated", "targets"], [[f"`{s['file']}`", f"`{s['commit']}`", str(s.get("capturedAt")), @@ -875,16 +875,16 @@ def render_markdown(report: dict[str, Any]) -> str: device = sim wall = "/".join(str(r.get("wallSeconds", "?")) for r in mine) counts = (f"{suite.get('tests', '?')}/{suite.get('failures', '?')}/{suite.get('skipped', '?')}" - if suite else "—") + if suite else ", ") rows.append([ f"`{name}`", f"`{target.task}`" if target else "?", status if not reason else f"**{status}**", - device or ("—" if not (target and target.avd) else "—"), - platform_name or "—", + device or (", " if not (target and target.avd) else ", "), + platform_name or ", ", counts, - wall or "—", - conditions or "—", + wall or ", ", + conditions or ", ", ]) lines += _table( ["target", "task", "status", "device / simulator", "platform, self-reported", @@ -895,7 +895,7 @@ def render_markdown(report: dict[str, Any]) -> str: notes = [(n, r) for n, r in notes if r] if notes: lines += ["**Targets that did not produce numbers, and why:**", ""] - lines += [f"- `{n}` — {r}" for n, r in notes] + lines += [f"- `{n}`, {r}" for n, r in notes] lines.append("") # ---- upcall -------------------------------------------------------------------- @@ -913,7 +913,7 @@ def render_markdown(report: dict[str, Any]) -> str: lines += _table(["target"] + [c for c, _ in UPCALL_COLUMNS], rows) lines.append("") - lines += ["Controls, from the same block — these are what say whether a row moved because the " + lines += ["Controls, from the same block, these are what say whether a row moved because the " "boundary moved or because the whole host did:", ""] rows = [] for name in targets: diff --git a/benchmarks/rn-benchmark/COMPARISON.md b/benchmarks/rn-benchmark/COMPARISON.md index b3a8d446f..fb95f338c 100644 --- a/benchmarks/rn-benchmark/COMPARISON.md +++ b/benchmarks/rn-benchmark/COMPARISON.md @@ -14,11 +14,11 @@ its numbers are read, not reproduced. `docs/cost-table.md`, whose `iosSimulatorArm64` row was captured at **`958c0082b294`** at load [1.97, 2.29, 5.92], marked uncontaminated, from three runs. §4b and §1c use it rather than `docs/design/upcall.md` because it is the only reference-side source that publishes an iOS upcall, its -same-run pure-Python callee control and its `withPython` scope figure together — the three cells the +same-run pure-Python callee control and its `withPython` scope figure together, the three cells the comparison needs from one execution. Its upcall figure (2253.20–2312.97 ns) and §4a's quoted 2266–2301 ns are close but **are not the same measurement**; neither is derived from the other. -**The iOS column is now measured** — `results/ios-20260814-200903.txt`, iPhone 17 Pro simulator on +**The iOS column is now measured**, `results/ios-20260814-200903.txt`, iPhone 17 Pro simulator on iOS 26.2, load average 3.79 at the start and 3.33 at the end, under the 4.0 ceiling. §7, which used to say iOS could not be built on this machine, was **wrong about why**, and the correction is recorded there rather than deleted. **The Android column is still 미측정** (unmeasured / 인용 불가): @@ -29,7 +29,7 @@ recorded there rather than deleted. **The Android column is still 미측정** (u Read the iOS column with one caveat the run states about itself, and only one: its **Hermes JS-only baseline did not settle** far enough before the warmup (1.11x margin against a 1.5x bar), so the JS -rows and every ratio that divides by one carry that. **The boundary rows do not** — the boundary +rows and every ratio that divides by one carry that. **The boundary rows do not**, the boundary sweep settled after 10 000 calls with a 10x margin, which is what an AOT callee with no tier-up should do, and the absolute figures do not depend on the JS baseline at all. §4b is the table that matters and it is made of absolute nanoseconds for exactly this reason. @@ -57,7 +57,7 @@ the seam, and gets a value back before the next bytecode runs. Neither queues, n neither involves a scheduler. The correspondence goes deeper than the description. Both projects pay a **different number of -boundaries per platform, and it is the same difference on the same platforms** — established here +boundaries per platform, and it is the same difference on the same platforms**, established here by reading generated code rather than documentation: | | React Native | PythonMultiplatform | @@ -90,13 +90,13 @@ crossing-plus-scheduler-plus-microtask-drain. | direction | RN | ours | comparable? | |---|---|---|---| -| script → host, synchronous | sync TurboModule method (returns a value) | upcall (`trampoline.add`) | **yes — this is the comparison** | +| script → host, synchronous | sync TurboModule method (returns a value) | upcall (`trampoline.add`) | **yes, this is the comparison** | | host → script, synchronous | *does not exist for app code* | downcall (`PyObject_CallObject`) | **no** | | host → script, asynchronous | `Callback` / `Promise` round trip | (no equivalent; the reference project has no async downcall path) | **no, in the other direction** | | fire-and-forget script → host | `void` TurboModule method | (no equivalent) | no | -A JSI-level synchronous native → JS call *is* possible — `jsi::Function::call` from a C++ -TurboModule while on the JS thread — and that would be the true mirror. It needs a pure C++ module +A JSI-level synchronous native → JS call *is* possible, `jsi::Function::call` from a C++ +TurboModule while on the JS thread, and that would be the true mirror. It needs a pure C++ module rather than the Kotlin/ObjC one built here. Recorded as the next thing to add, not done. ### 1c. Row for row: what each row here must be read against, and on which ratio @@ -112,17 +112,17 @@ Reference-side sources: `UpcallBoundaryCostTest` (`commonTest`, quoted through | row here | what it charges | reference-project counterpart | same direction? | comparable? | |---|---|---|---|---| -| `addInts(3, 4)` | JS → native sync crossing, two doubles in, one out | **`trampoline.add`, "upcall to Kotlin"** — two ints in, one out, driven from Python | yes, script → host | **yes. This is the row the cross-project comparison is made on.** | -| `zeroArgs()` | the same crossing with no arguments | *none.* The nearest is `UpcallTrampoline.invoke` with the GIL held, but that is the marshalling **with the boundary removed**, not the boundary with the marshalling removed — the opposite subtraction | yes | only as `addInts − zeroArgs`, i.e. what two numbers cost. The absolute value has nothing to sit beside | -| `stringLength(8 / 128)` | one string conversion inward | `PyUnicode_AsUTF8 (8 chars)` in `overhead/BenchmarkTest` (78–84 ns desktop at the settled warmup) | **no** — that is a downcall | shape only. Do not put them in one table cell | +| `addInts(3, 4)` | JS → native sync crossing, two doubles in, one out | **`trampoline.add`, "upcall to Kotlin"**, two ints in, one out, driven from Python | yes, script → host | **yes. This is the row the cross-project comparison is made on.** | +| `zeroArgs()` | the same crossing with no arguments | *none.* The nearest is `UpcallTrampoline.invoke` with the GIL held, but that is the marshalling **with the boundary removed**, not the boundary with the marshalling removed, the opposite subtraction | yes | only as `addInts − zeroArgs`, i.e. what two numbers cost. The absolute value has nothing to sit beside | +| `stringLength(8 / 128)` | one string conversion inward | `PyUnicode_AsUTF8 (8 chars)` in `overhead/BenchmarkTest` (78–84 ns desktop at the settled warmup) | **no**, that is a downcall | shape only. Do not put them in one table cell | | `echoString(8 / 128)` | two string conversions | same as above, doubled | **no** | shape only | -| `sumArray(8 / 1000)` | JSI Array → `ReadableArray`, eager, per element | `list → LongArray`, 1000 elements: 50065.89 → 4532.55 ns after composition | **no** — downcall | **slope only.** The per-element slope is the transferable quantity; the intercepts are not | -| `sumObject(3 keys)` | JSI Object → `ReadableMap` | `getAttr` composition, 3929.84 → 713.34 ns | **no** — downcall | slope/composition ratio only | -| `nativeLoopNs(n)` | the callee with the crossing removed, native clock | `_pm_time_loop` (empty Python loop) and `_pm_py_add` (pure-Python callee), both timed inside Python | n/a — both are floors | **yes in role**, not in value: each says "what this side costs before anything crosses" | +| `sumArray(8 / 1000)` | JSI Array → `ReadableArray`, eager, per element | `list → LongArray`, 1000 elements: 50065.89 → 4532.55 ns after composition | **no**, downcall | **slope only.** The per-element slope is the transferable quantity; the intercepts are not | +| `sumObject(3 keys)` | JSI Object → `ReadableMap` | `getAttr` composition, 3929.84 → 713.34 ns | **no**, downcall | slope/composition ratio only | +| `nativeLoopNs(n)` | the callee with the crossing removed, native clock | `_pm_time_loop` (empty Python loop) and `_pm_py_add` (pure-Python callee), both timed inside Python | n/a, both are floors | **yes in role**, not in value: each says "what this side costs before anything crosses" | | JS-only rows (`jsRows.js`) | the language floor: loop, call, array, object | `_pm_time_loop` 8–9 ns and `_pm_py_add` 24–27 ns on desktop | n/a | **yes, and both projects require them from the same run as the boundary row** | | `noop()` (`void`) | an enqueue onto the module's method queue | *none* | fire-and-forget | **no** | -| callback / Promise round trips | crossing + `RuntimeScheduler` hop + microtask drain | *none* — the reference project has no async downcall path | host → script, async | **no** | -| — | | `Python3.withPython { }`, empty: **50–57 ns** desktop, **276–303 ns** ART `pmp_api36` | charged to every reference crossing in both directions | **no counterpart here at all.** React Native has no GIL | +| callback / Promise round trips | crossing + `RuntimeScheduler` hop + microtask drain | *none*, the reference project has no async downcall path | host → script, async | **no** | +| - | | `Python3.withPython { }`, empty: **50–57 ns** desktop, **276–303 ns** ART `pmp_api36` | charged to every reference crossing in both directions | **no counterpart here at all.** React Native has no GIL | #### The ratio that actually transfers, and it is not the one either table leads with @@ -132,7 +132,7 @@ the other would be the exact mistake this section exists to prevent: React Nativ host → script call to be the denominator of the first (§1b), so that ratio cannot be formed here. What *can* be formed on both sides is **boundary cost ÷ the script language's own call of the same -shape** — `addInts / jsCall` here, `upcall / _pm_py_add` there. Both are a pure number, both come +shape**, `addInts / jsCall` here, `upcall / _pm_py_add` there. Both are a pure number, both come from one run, and both answer "what multiple of a normal call in this language does crossing cost". Computed here from two rows the reference project publishes from the same runs (it publishes the @@ -145,7 +145,7 @@ rows, not this ratio): | iOS simulator (`docs/cost-table.md` @ `958c0082`) | 2253.20–2312.97 ns | 36.80–38.32 ns | **58.8–62.9x** | Two very different platforms landing in the same band is what made this worth using as the -cross-project quantity — **and the iOS row, added once it could be measured, does not land in that +cross-project quantity, **and the iOS row, added once it could be measured, does not land in that band.** 59–63x against 19–23x is not a measurement problem: the reference project's iOS upcall is genuinely ~4x its desktop one while CPython's own call barely moves, so the ratio moves with it. Whatever this quantity normalises away, it does not normalise away the platform. @@ -158,11 +158,11 @@ So on the one platform where both sides exist, the ratio says RN's boundary is t relative to its own language (82–84x against 59–63x) while the absolute nanoseconds say the opposite (1.10 µs against 2.25–2.31 µs). **Both are true and §4b is where they are reconciled.** The reason is the warning immediately below, arriving exactly as predicted: the denominators are *not* within -30% of each other here — CPython's call is 2.8x Hermes' — so this ratio has stopped transferring on +30% of each other here, CPython's call is 2.8x Hermes', so this ratio has stopped transferring on this platform, and the absolute table is the one to read. > **Read the denominator before the ratio.** Hermes' function call is ~33 ns and CPython's is -> ~25 ns on desktop, so the two denominators are within about 30% of each other — which is the only +> ~25 ns on desktop, so the two denominators are within about 30% of each other, which is the only > reason this ratio is meaningful across the projects at all. If the RN column were measured under > JSC or V8 (~4–10 ns) the same boundary would show a 3–8x larger ratio without anything about the > boundary changing. §6.5 is the long form of this. @@ -194,13 +194,13 @@ stated convention. --- -## 3. What warmup React Native needs — measured, and the answer is not the JVM's +## 3. What warmup React Native needs, measured, and the answer is not the JVM's The reference project's 100 000-call warmup is not a round number: its docs record a 40-repetition sweep in which the JVM read 1307, 716, 696, 660, 548 and then flattened at ~535 ns per 10 000 calls, converging on the same plateau from the warm side too, and it attributes that to host JIT tier-up (C2 on the JVM, V8's tiering on wasm). Its pure-Python control rows, by contrast, were flat -from the first repetition — which is what identified the warming as host-side rather than +from the first repetition, which is what identified the warming as host-side rather than CPython-side. **The same sweep was run here on Hermes and on V8 from the same source file.** Both series are 40 @@ -226,7 +226,7 @@ V8, cold: 18.08 14.93 2342.30 28.74 20.68 20.64 20.69 20.63 **Findings, stated separately from what they do and do not license:** -1. **Hermes does not tier up.** Eighty consecutive readings, cold and warm, span 32.47–32.96 ns — +1. **Hermes does not tier up.** Eighty consecutive readings, cold and warm, span 32.47–32.96 ns, a 1.5% band, with the first reading inside it. There is no plateau to reach because there is no compilation tier to reach it from: Hermes ships without a JIT. **The reference project's reason for warming 100 000 calls does not exist on this engine.** @@ -235,7 +235,7 @@ V8, cold: 18.08 14.93 2342.30 28.74 20.68 20.64 20.69 20.63 point), and an earlier run of the identical script stepped down *further*, to 0.58 ns, at a different repetition. One reading in the series above is 2342.30 ns. V8's spread across repetitions is larger than most of the effects this benchmark is trying to measure. -3. **The warmup stays at 100 000**, and the reason is stronger than the two originally given here +3. **The warmup stays at 100 000**: and the reason is stronger than the two originally given here (that the engine is a build-time choice, and that the warmup is free on Hermes anyway). Both are true, but they are "in case" arguments. The real reason is in 3a below: **this sweep only prices half of the boundary, and the half it cannot reach is the half that tiers up.** @@ -247,13 +247,13 @@ V8, cold: 18.08 14.93 2342.30 28.74 20.68 20.64 20.69 20.63 **Caveat, and it is a real one.** The only standalone Hermes VM obtainable for this machine is `hermes-engine-cli@0.12.0` (2022-era, RN 0.68 vintage), while the app ships RN 0.87's much newer Hermes. Absolute ns figures from the host binary must **not** be quoted as the app's. What survives -the version gap is the *structural* finding — no JIT, therefore no tier-up, therefore no warmup -curve — and that has been Hermes' design throughout. +the version gap is the *structural* finding, no JIT, therefore no tier-up, therefore no warmup +curve, and that has been Hermes' design throughout. ### 3a. The sweep above prices half the boundary, and the other half is on ART **"Hermes has no JIT, so no warmup is needed" does not follow, and on Android it is wrong.** The -reference project's warmup exists because of *host JIT tier-up* — its own words: "C2 on the JVM, +reference project's warmup exists because of *host JIT tier-up*, its own words: "C2 on the JVM, V8's tiering on wasm". The question is therefore not "does the script engine tier up" but "does **anything on the call path** tier up". On Android, the call path is: @@ -273,16 +273,16 @@ in its entire table** (`docs/design/upcall.md` §7.2): | androidNative `pmp_api36` | 1.27x | ~40 000 | 2.5x | | androidNative `pmp_api26` | 1.30x | ~30 000 | 3.3x | | ART `pmp_api26` | 1.93x | ~20 000, noisily | 5x | -| **ART `pmp_api36`** | **8.56x** | **~90 000–100 000** | **~1x — none** | +| **ART `pmp_api36`** | **8.56x** | **~90 000–100 000** | **~1x, none** | Its `pmp_api36` ART series reads 9194.8, 3218.9, 2288.7, 1648.3, 1218.3, 1173.3, 1129.5, 1532.7, -1057.8, 1044.6 against a plateau of 1074.2 — **still 9% high at 70 000 calls**, and note the 1532.7 +1057.8, 1044.6 against a plateau of 1074.2, **still 9% high at 70 000 calls**, and note the 1532.7 at repetition 8, after four readings that were already closer. A series that dips toward the plateau and back out has not settled. So the RN Android boundary rows sit on the same curve, for the same reason, on the same runtime, at the same API level. **100 000 is not margin here; it is roughly exactly enough**, and that is the -finding — not "Hermes needs no warmup". +finding, not "Hermes needs no warmup". **Two things follow, and both were wrong before this pass:** @@ -296,7 +296,7 @@ finding — not "Hermes needs no warmup". computes, from that run's own sweeps, where the series settled and what margin 100 000 has over it. If the margin is under 1.5x, or the series never settles within ±5%, the report says in as many words that its own boundary rows must not be quoted. The threshold is printed with the - answer because the answer depends on it — at ±5% the reference ART series settles at ~90 000 + answer because the answer depends on it, at ±5% the reference ART series settles at ~90 000 and at ±10% at ~50 000, and a settle point quoted without its band is not reproducible. **The iOS half is now answered, and it lands on top of the reference project's iOS row:** @@ -306,7 +306,7 @@ finding — not "Hermes needs no warmup". | reference project, iOS simulator (quoted, top of this section) | 1.01x | ~10 000 | 10x | | **RN `addInts` through the TurboModule, iOS simulator (measured today)** | **1.05x** | **10 000** | **10.00x** | -Two different projects, two different script engines, two different host languages — and the same +Two different projects, two different script engines, two different host languages, and the same settling behaviour on the same platform, because on iOS *neither* project has a tiering runtime on the far side of the boundary. That is the control that makes §3a's Android argument falsifiable rather than merely plausible: the claim is that ART's tier-up is what forces the large warmup, and @@ -315,7 +315,7 @@ the platform without ART shows no warm-up curve at all in either project. **What is still not known:** whether RN 0.87's Hermes, on the shipping build, behaves like the 2022 standalone binary, and where the ART-backed boundary row actually settles on this hardware. Both need one emulator and one `./run-bench.sh android`. **Neither has been measured**, and the iOS -result above says nothing about them — it is the arm of the experiment where the effect is expected +result above says nothing about them, it is the arm of the experiment where the effect is expected to be absent. The 100 000 is still carried over from the reference project's ART measurement, which is a defensible prior and not a result. @@ -328,7 +328,7 @@ sweep did not settle within a 1.5x margin on either iOS run (1.11x and 1.43x, at ^ one repetition, and it sets "flat from 90 000" ``` -The first repetition is **1.02x the plateau** — flat from the start. What sets the settle point is a +The first repetition is **1.02x the plateau**, flat from the start. What sets the settle point is a **single 15.37 ns excursion at repetition 9**, and `analyzeSweep` reports the *last* reading outside ±5%, so one outlier anywhere early moves "flat from" to its position. The step down to ~13.57 in the middle third and back to ~14.1 by the end is frequency scaling, not warming: a series that warms @@ -337,8 +337,8 @@ does not go back up. **So read "flat from" as an upper bound that a single noisy repetition can dominate, not as a measured settle point.** The same fragility bit the boundary sweep on the load-5.8 run, where a spike at repetition 30 produced "flat from 320 000" for a series whose first repetition was 0.99x -its plateau. The metric is left as it is — it is the reference project's, and changing it here would -break the comparison — but the sweep series is printed in full in every report precisely so this is +its plateau. The metric is left as it is, it is the reference project's, and changing it here would +break the comparison, but the sweep series is printed in full in every report precisely so this is checkable rather than taken on trust. The practical consequence: **a quiet machine matters more for the JS baseline than for the boundary @@ -373,7 +373,7 @@ project's Android column is compared against**, and it is the one that was most under-warmed. ‡ and ※ mark rows where upstream kept a single outlying run rather than dropping it; see the source -document. `wasmJs`'s absolute column is only meaningful as a full-suite figure — its ratio column is +document. `wasmJs`'s absolute column is only meaningful as a full-suite figure, its ratio column is the quotable one there. Per-crossing FFI cost, once the desktop path was fixed (`docs/downcall-design.md`): @@ -387,7 +387,7 @@ Per-crossing FFI cost, once the desktop path was fixed (`docs/downcall-design.md The language floor those sit on, **corrected**: on desktop an empty Python loop is 8–9 ns and a pure-Python callee of the same shape as the upcall is **24–27 ns**; on wasmJs they are 14–16 ns and 47–54 ns (`docs/downcall-design.md`). On ART `pmp_api36` they are 17.3 ns and 49.7–50.4 ns. -**Both are flat from the first 10 000 calls on every host and device** — the control that identified +**Both are flat from the first 10 000 calls on every host and device**, the control that identified the warming as host-side rather than CPython-side. > An earlier version of this line gave the desktop pure-Python callee as 8.4–8.6 ns. That is the @@ -404,7 +404,7 @@ native → JS direction has no cell here at all. | | React Native (this repo) | PythonMultiplatform (quoted) | |---|---|---| -| the call | `Bench.addInts(3, 4)` — JS → ObjC++ TurboModule | Python → Kotlin upcall | +| the call | `Bench.addInts(3, 4)`, JS → ObjC++ TurboModule | Python → Kotlin upcall | | **cost of one crossing** | **1101.28 – 1119.40 ns** | **2253.20 – 2312.97 ns** | | the script language's own call, same shape, same run | 13.37 – 13.39 ns (Hermes) | 36.80 – 38.32 ns (CPython) | | crossing ÷ that call | 82.25 – 83.72x | 58.80 – 62.85x | @@ -417,11 +417,11 @@ native → JS direction has no cell here at all. Two things must be said about that number, in this order. **It is not a like-for-like verdict on the two designs.** §1a establishes that on iOS *neither* -project pays a second managed-runtime crossing — RN does one ObjC message send, ours calls a -`staticCFunction` in the same binary — so the 2x is not "one extra boundary". What it prices is the +project pays a second managed-runtime crossing, RN does one ObjC message send, ours calls a +`staticCFunction` in the same binary, so the 2x is not "one extra boundary". What it prices is the two runtimes' cost of *arriving at* their host function: CPython's calling convention, argument tuple and GIL discipline against Hermes' JSI dispatch. The reference project's own controls locate -most of ours: an empty `withPython` scope alone is **701.18 – 723.85 ns** on this target — **63 – 66% +most of ours: an empty `withPython` scope alone is **701.18 – 723.85 ns** on this target, **63 – 66% of RN's entire `addInts` call**, spent by us before a single argument is converted. That, not the crossing, is where the 2x lives. @@ -429,14 +429,14 @@ crossing, is where the 2x lives. CPython calls' worth of crossing; RN's is 82.3 – 83.7 Hermes calls' worth. Ours is cheaper *relative to its own language* and dearer *in nanoseconds*, because CPython's own call (36.8 – 38.3 ns) is about 2.8x Hermes' (13.4 ns). §1c is explicit that this ratio only transfers between projects while -the two denominators are close, and here they differ by nearly 3x — so **the absolute row is the +the two denominators are close, and here they differ by nearly 3x, so **the absolute row is the representative one and the ratio row is the supporting one**, not the reverse. A reader who quotes only the ratio will conclude ours is the cheaper boundary, which on this platform it is not. > The ratio row inherits the run's own caveat: the Hermes baseline sweep did not settle (1.11x -> margin). The absolute RN row does not — its sweep settled at 10 000 calls with 10x margin. +> margin). The absolute RN row does not, its sweep settled at 10 000 calls with 10x margin. -### 4c. React Native — measured on iOS, 미측정 on Android +### 4c. React Native, measured on iOS, 미측정 on Android The iOS column is filled from `results/ios-20260814-200903.txt`. Ranges are min–max over 5 repetitions inside that single process; nothing is averaged across runs and nothing is discarded. @@ -448,18 +448,18 @@ The Android column is marked 미측정 (인용 불가): 2026-08-18/19에 수행 | empty JS loop | 미측정 | 13.68 – 14.08 ns | | JS call, same shape as `addInts` | 미측정 | 13.37 – 13.39 ns † | | **JS → native, synchronous** | | | -| `zeroArgs()` — boundary only | 미측정 | 975.82 – 985.27 ns | -| `addInts(3, 4)` — two numbers | 미측정 | **1101.28 – 1119.40 ns** | -| `stringLength(8 chars)` — one conversion | 미측정 | 1376.30 – 1377.83 ns | +| `zeroArgs()`, boundary only | 미측정 | 975.82 – 985.27 ns | +| `addInts(3, 4)`, two numbers | 미측정 | **1101.28 – 1119.40 ns** | +| `stringLength(8 chars)`, one conversion | 미측정 | 1376.30 – 1377.83 ns | | `stringLength(128 chars)` | 미측정 | 1783.75 – 1844.78 ns | -| `echoString(8 chars)` — two conversions | 미측정 | 1428.03 – 1479.65 ns | +| `echoString(8 chars)`, two conversions | 미측정 | 1428.03 – 1479.65 ns | | `echoString(128 chars)` | 미측정 | 1882.72 – 1916.37 ns | | `sumArray(8 elements)` | 미측정 | 2024.79 – 2065.26 ns | | `sumArray(1000 elements)` | 미측정 | 64584.08 – 65445.09 ns | | `sumObject(3 keys)` | 미측정 | 2250.80 – 2270.83 ns | | **controls** | | | | `addInts` body, native loop, no crossing | 미측정 | 3.50 – 3.54 ns | -| `noop()` — `void`, async dispatch, *not* a crossing | 미측정 | 1421.74 – 1514.50 ns | +| `noop()`, `void`, async dispatch, *not* a crossing | 미측정 | 1421.74 – 1514.50 ns | | **native → JS ROUND TRIPS (not comparable with the above)** | | | | callback round trip | 미측정 | 13642.42 – 14032.71 ns | | Promise round trip | 미측정 | 13868.88 – 14646.50 ns | @@ -467,7 +467,7 @@ The Android column is marked 미측정 (인용 불가): 2026-08-18/19에 수행 | **convergence sweeps (cold, 40 x 10 000), see §3a** | | | | JS-only row: settles after how many calls | 미측정 | 90 000 † | | **`addInts` through the TurboModule: settles after how many calls** | 미측정 | **10 000** | -| margin the 100 000 warmup has over that | 미측정 | JS-only **1.11x — too thin †**; boundary **10.00x — adequate** | +| margin the 100 000 warmup has over that | 미측정 | JS-only **1.11x, too thin †**; boundary **10.00x, adequate** | | **ratios, all terms from the same run** | | | | **`addInts` / a JS call of the same shape** ← the cross-project ratio (§1c) | 미측정 | 82.25 – 83.72x † | | `addInts` / `zeroArgs` (what two numbers cost) | 미측정 | 1.13x (116.01 – 143.58 ns) † | @@ -475,25 +475,25 @@ The Android column is marked 미측정 (인용 불가): 2026-08-18/19에 수행 | `echoString(128)` / `echoString(8)` (per-character slope) | 미측정 | 1.32x | † **carries the run's own caveat.** The Hermes JS-only sweep settled only at 90 000 calls against a -100 000 warmup — a 1.11x margin, under the 1.5x bar the report applies — so the JS baseline row and +100 000 warmup, a 1.11x margin, under the 1.5x bar the report applies, so the JS baseline row and every ratio computed against it are marked. This is a statement about the *denominator*, not about the boundary: the boundary sweep in the same run settled at 10 000 calls with a 10x margin, and its first repetition was 1.05x the plateau. On iOS the callee is AOT-compiled Objective-C++ with no tier-up, so there is no warm-up curve for it to climb; the JS-side drift (14.15 → 13.57 ns across the sweep) has the shape of CPU frequency scaling on a machine that was not idle, not of a warmup -that is too short. Re-running quieter is the fix, **not** raising `WARMUP` — see §3a for why that +that is too short. Re-running quieter is the fix, **not** raising `WARMUP`, see §3a for why that number has no room in it on the Android side. **What the iOS column says on its own,** before any cross-project reading: -- **The crossing dominates everything it carries.** `zeroArgs` — the boundary with no arguments at - all — is 975.82 ns, which is 88% of `addInts`. Two integers cost 116 – 144 ns on top of it. +- **The crossing dominates everything it carries.** `zeroArgs`, the boundary with no arguments at + all, is 975.82 ns, which is 88% of `addInts`. Two integers cost 116 – 144 ns on top of it. Anything that reduces the *number* of calls pays; anything that reduces the *arguments* barely does. - **Array elements are priced like arguments, not like bulk data.** `sumArray` goes 2024.79 → 64584.08 ns from 8 to 1000 elements: **63.1 ns per element**, which is the same order as the - 58 – 72 ns each of `addInts`' two integers costs. Strings are ~17x cheaper per unit — - `echoString` at 8 → 128 characters is 1.32x, or **3.6 – 3.8 ns per character** — so a 1000-element + 58 – 72 ns each of `addInts`' two integers costs. Strings are ~17x cheaper per unit, + `echoString` at 8 → 128 characters is 1.32x, or **3.6 – 3.8 ns per character**, so a 1000-element array is the row to avoid, and a long string is not. - **The async round trips are ~12x a synchronous crossing** (13.6 – 14.6 µs against 1.10 µs) and most of that is not the boundary: a bare `await Promise.resolve(1)` with no native code in it is @@ -507,7 +507,7 @@ blended number: `sumArray` at 8 and 1000 separates the per-call boundary from th conversion, and `echoString` at 8 and 128 does the same for characters. The reference project's `getAttr` and `list → LongArray` composition rows are the equivalent measurement on its side. -Host-side JS floor, **measured** (host binaries, not the app — see the caveat in section 3): +Host-side JS floor, **measured** (host binaries, not the app, see the caveat in section 3): | row | Hermes 0.12.0 | Node 24.19.0 / V8 13.6 | |---|---|---| @@ -533,7 +533,7 @@ and it is **not uniform, which is itself the finding**: on min-to-min, | 1000-element array sum | 23.4x | Hermes' bytecode dispatch is within striking distance of V8's on a bare loop and falls behind by an -order of magnitude the moment property access and array indexing are involved — which is exactly +order of magnitude the moment property access and array indexing are involved, which is exactly what a JIT with shape feedback buys and an interpreter cannot. That shape matters for the boundary rows still to be measured: the `sumArray` rows have the most JS-side work in them, so their *JS* half is where Hermes is weakest, and a boundary cost read as a ratio against them will look @@ -543,7 +543,7 @@ correspondingly small. ## 5. Size and startup -### 5a. Size — measured on both sides, but they are not the same kind of artefact +### 5a. Size, measured on both sides, but they are not the same kind of artefact **Read this comparison as two different questions.** The reference project's 23.42 MB is a *library AAR* that a consumer app adds; the RN figure is a *whole application*. They are placed @@ -552,7 +552,7 @@ denominators differ and neither number should be lifted out of its row. | | measured | note | |---|---|---| -| **PythonMultiplatform AAR** (release, 2 ABIs) | **23.42 MB** on disk, 22.04 MB of entries, 1740 files | after removing CPython's own test suite and C headers; was 40.10 MB. The library's own classes are **0.49 MB** of it — the rest is CPython plus its stdlib. `ROADMAP.md` §15d. | +| **PythonMultiplatform AAR** (release, 2 ABIs) | **23.42 MB** on disk, 22.04 MB of entries, 1740 files | after removing CPython's own test suite and C headers; was 40.10 MB. The library's own classes are **0.49 MB** of it, the rest is CPython plus its stdlib. `ROADMAP.md` §15d. | | **RN release APK** (arm64-v8a only, this app) | **17.71 MB** on disk, 24.80 MB raw | measured here: `./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a` | | RN release APK (all four ABIs) | 51.56 MB on disk | measured here; this is what `run-bench.sh android` builds, since it does not know the device's ABI in advance. An App Bundle strips this per device; the AAR row above does **not** get that treatment for its `assets/`, which is the reference project's §15d finding. | | RN debug APK (arm64-v8a only) | 38.84 MB on disk, 45.32 MB raw | measured here | @@ -574,8 +574,8 @@ And what is inside ours, per `ROADMAP.md`'s breakdown of the pre-trim AAR (compr `jni/` 5.09–5.31 MB, pure-Python stdlib 3.16 MB, `lib-dynload` 2.37–2.46 MB. **The shapes are different in a way worth saying out loud.** RN's runtime cost is dominated by -*engine and framework code* — 11.1 MB of dex plus 11.1 MB of native library, for 1.0 MB of actual -application. Ours is dominated by *a language's standard library shipped as data* — 0.49 MB of +*engine and framework code*, 11.1 MB of dex plus 11.1 MB of native library, for 1.0 MB of actual +application. Ours is dominated by *a language's standard library shipped as data*, 0.49 MB of library for ~23 MB of CPython. An RN app that grows adds JS bytecode at roughly 1 MB per application; a PythonMultiplatform app that grows adds Python source, and the fixed CPython cost is already paid. @@ -584,8 +584,8 @@ already paid. | | ours (measured, `ROADMAP.md`) | RN | |---|---|---| -| first launch | **237 ms** (API 26) / **480 ms** (API 36) — unpacking 804 files, 20 147 280 bytes of stdlib from assets | 미측정 | -| every later launch | **4 ms** (API 26) / **28 ms** (API 36) — a stamp check | 미측정 | +| first launch | **237 ms** (API 26) / **480 ms** (API 36), unpacking 804 files, 20 147 280 bytes of stdlib from assets | 미측정 | +| every later launch | **4 ms** (API 26) / **28 ms** (API 36), a stamp check | 미측정 | The RN counterpart to measure is time-to-first-render, and the structural expectation to test is that it has **no equivalent of the first-launch unpack at all**: Hermes bytecode is `mmap`ed @@ -597,7 +597,7 @@ by path, and RN's is one bytecode blob. --- -## 6. What creates the cost — the part that transfers +## 6. What creates the cost, the part that transfers This is the section worth keeping. It does not need a device, because it is about mechanism. @@ -609,8 +609,8 @@ shim on Android and an `ObjCTurboModule`-based one on iOS. Anything that reduces on Android roughly twice as much as on iOS, in both projects. **2. Ours pays for something RN has no analogue of: the GIL, on every single call.** The reference -project measures an *empty* `Python3.withPython { }` scope — a GIL acquire/release with no C API -call in it — at 50–57 ns on desktop, 24–28 ns on wasm, and 725–982 ns on the iOS simulator. That is +project measures an *empty* `Python3.withPython { }` scope, a GIL acquire/release with no C API +call in it, at 50–57 ns on desktop, 24–28 ns on wasm, and 725–982 ns on the iOS simulator. That is charged to every crossing in both directions. React Native has no such lock: Hermes is single-threaded per runtime and a synchronous TurboModule call is already on the JS thread, so there is nothing to acquire. **A large part of what looks like "our boundary is expensive" is not @@ -618,21 +618,21 @@ the boundary; it is a design constraint of the runtime being embedded.** **3. RN pays for something we do not: everything crosses as a JSI value.** Our upcall passes a `PyObject*` and the callee decides what to do with it; ownership is explicit and nothing is copied -unless asked. An RN argument is converted at the seam — `jsi::String` to `NSString`/`jstring`, -`jsi::Array` to `NSArray`/`ReadableArray` — and the conversion is eager and per element. This is +unless asked. An RN argument is converted at the seam, `jsi::String` to `NSString`/`jstring`, +`jsi::Array` to `NSArray`/`ReadableArray`, and the conversion is eager and per element. This is exactly why `sumArray` is measured at 8 and at 1000 elements: the slope between them *is* that cost, and it is the row most likely to differ by an order of magnitude from ours. **4. Both projects' worst case is the same shape: many small crossings.** The reference project -measured composition against per-call — `getAttr` 3929.84 → 713.34 ns, `list → LongArray` at 1000 -elements 50065.89 → 4532.55 ns — and found that the win was mostly *redundant work removed*, not +measured composition against per-call, `getAttr` 3929.84 → 713.34 ns, `list → LongArray` at 1000 +elements 50065.89 → 4532.55 ns, and found that the win was mostly *redundant work removed*, not crossings removed. RN's architectural answer to the same problem is the same one: batch at the boundary. That agreement is worth more than either project's absolute numbers. **5. The engine underneath changes what "overhead" means.** Hermes has no JIT, so its JS floor is ~33 ns per call where V8's is ~4–10 ns. A 200 ns boundary is a 6x tax on Hermes and a 20–50x tax on V8. CPython's floor is 8–9 ns per bytecode loop iteration and **24–27 ns per pure-Python call** on -desktop — i.e. *within about 30% of Hermes*, not far below it. **That near-coincidence is the only +desktop, i.e. *within about 30% of Hermes*, not far below it. **That near-coincidence is the only reason §1c's cross-project ratio means anything**; under JSC or V8 the same boundary would score 3–8x worse against its floor without anything about the boundary having changed. **Any cross-project ratio must be read against the floor measured in the same run, which is why both harnesses measure @@ -660,12 +660,12 @@ The `-showdestinations` output was real; the inference from it was not. That com Xcode's **scheme-destination resolution**, which insists on a platform component matching the *current* SDK. Building the target against `-sdk iphonesimulator` with no destination does not go through that layer, and it works. The runtimes that were dismissed as "from older Xcode installs and -Xcode 26.6 will not use them" — iOS 26.2 among them — are exactly the runtimes the app now runs on. +Xcode 26.6 will not use them", iOS 26.2 among them, are exactly the runtimes the app now runs on. **The lesson is about the test, not about Xcode.** `-showdestinations` was treated as a capability probe for the machine when it is a resolution probe for a scheme, and a negative result from it was generalised to "any project on this machine including CocoaPods' own targets". A capability claim -that broad should have been falsified by attempting the build once — which costs minutes and is what +that broad should have been falsified by attempting the build once, which costs minutes and is what `build-ios.sh` now does. `run-bench.sh` no longer consults `-showdestinations` at all; the comment in `cmd_ios` records why, so this cannot be reintroduced as a fast pre-check. @@ -673,7 +673,7 @@ in `cmd_ios` records why, so this cannot be reintroduced as a fast pre-check. Two `./run-bench.sh ios` runs **exited 0 with results files containing the conditions block and not one benchmark row** (`results/ios-20260814-171508.txt`, 22 lines; `results/ios-20260814-183543.txt`, -19 lines). The app built, installed and launched correctly in both, and — as far as can be told — ran +19 lines). The app built, installed and launched correctly in both, and, as far as can be told, ran the entire benchmark correctly in both. The reports were never captured. **Cause: `log stream` defaults to `--level default`, which streams Default, Error and Fault and @@ -682,7 +682,7 @@ silently drops Info and Debug.** React Native's iOS logger emits JS `console.log messages had already been excluded from. logcat has no equivalent default filter, so the Android path never had this problem and the harness had never needed to think about levels. -**Confirmed by A/B on a single launch** — same app, same predicate, two `log stream` processes +**Confirmed by A/B on a single launch**, same app, same predicate, two `log stream` processes attached at once: | `log stream` invocation | lines matching `RNBENCH\|` | @@ -711,14 +711,14 @@ than a missing file: it looks like a measurement until someone reads it**, and i | condition | verdict | exit | |---|---|---| | zero benchmark rows | `FAILED`, with the three causes in the order they have actually occurred here | non-zero | -| rows but no `END` marker | `INCOMPLETE` — capture window too short, or the app died partway | non-zero | -| no "all N rows returned a positive duration" | `SUSPECT` — look for `UNUSABLE ROWS` | non-zero | +| rows but no `END` marker | `INCOMPLETE`, capture window too short, or the app died partway | non-zero | +| no "all N rows returned a positive duration" | `SUSPECT`, look for `UNUSABLE ROWS` | non-zero | | the run's own warmup verdict is `INSUFFICIENT` | `NOT QUOTABLE AS A WHOLE`, naming which sweep and therefore which rows | non-zero | | otherwise | rows and `END` recorded in a `capture check` block | 0 | A "row" is matched as `name | min | max | reps` with three numeric fields, not counted as lines: the capture also yields `#` progress lines and prose, and a run that printed only progress before dying -is exactly as empty as one that printed nothing. **Verified against all four states** — the two +is exactly as empty as one that printed nothing. **Verified against all four states**, the two empty files above return non-zero, a file with rows and `END` returns 0, and the same file with `END` stripped returns non-zero. @@ -730,7 +730,7 @@ warning eleven lines above it is the same failure as an empty file**, and it was One further race was closed: the harness slept a fixed 2 seconds after starting `log stream` before launching the app. It now waits for the stream's own `Filtering the log data using …` banner, up to 30 s, and records a skip if it never arrives. A lost race there produces an empty capture of a -healthy run — indistinguishable from the bug above. +healthy run, indistinguishable from the bug above. ### 7d. Still blocked @@ -759,12 +759,12 @@ healthy run — indistinguishable from the bug above. - **다만 워밍업은 JS 쪽에서는 실제로 부족했습니다 (새 사실, 2026-08-19T09:55)**: 같은 회차에서 JS-only sweep 이 `settles at 150000 calls, so warmup 100000 has only 0.67x margin -- TOO THIN` 로 판정됐습니다. 이것은 boundary 미수렴과 **별개의 결함**이며, JS 기준선과 그것으로 나눈 **모든 비율**을 - 무효화합니다(절대 boundary 수치는 무효화하지 않습니다 — 그쪽은 기준선에 의존하지 않습니다). + 무효화합니다(절대 boundary 수치는 무효화하지 않습니다, 그쪽은 기준선에 의존하지 않습니다). `appRun.js` 의 `WARMUP` 은 "sweep 이 달리 말하지 않는 한 그대로 둔다"는 자기 규칙을 달고 있었고, sweep 이 달리 말했으므로 **400,000 으로 올렸습니다.** 400,000 은 00:50 회차가 이미 그 sweep 에 충분한 여유를 준다고 보인 값입니다. boundary 쪽은 이것으로 고쳐지지 않습니다. -- **네 번째 회차 (2026-08-23, WARMUP=400,000, load 2.71, 단독 실행) — 미수렴은 수렴 문제가 아니었습니다.** +- **네 번째 회차 (2026-08-23, WARMUP=400,000, load 2.71, 단독 실행), 미수렴은 수렴 문제가 아니었습니다.** 이번에는 부하 가드를 고친 뒤(아래 참조) 실제로 조용한 상태에서 돌렸고, 판정은 여전히 양쪽 다 `NEVER SETTLED` 였습니다. **그런데 sweep 의 원자료를 보면 수렴하지 않은 곡선이 아니라 이미 평탄한 바닥에 스파이크가 얹힌 계열입니다:** @@ -775,13 +775,13 @@ healthy run — indistinguishable from the bug above. | boundary (Hermes+JSI+JNI+ART) | 233.07 ns | 305.06 | **1080.88** | 13/40 | 13/40 | JS-only 계열에는 `10.63 10.64 10.63 10.86` 처럼 소수점 둘째 자리까지 반복되는 구간이 있습니다. - Hermes 는 JIT 이 없어 **1회차부터 평탄한 것이 정상**이고, 실제로 평탄합니다 — + Hermes 는 JIT 이 없어 **1회차부터 평탄한 것이 정상**이고, 실제로 평탄합니다, `first repetition 10.74 ns` 가 이미 바닥입니다. 깨는 것은 수렴 부족이 아니라 **단발 스파이크**이고, "40회 연속 ±5%" 기준은 스파이크 **한 번**이면 무조건 깨집니다. **이것이 워밍업을 4배로 올려도 나아지지 않은 이유입니다.** 워밍업을 늘려도 스케줄러 스파이크는 사라지지 않습니다. 8/19 회차에서 100k 로 JS-only 가 "150,000 에서 수렴"으로 나온 것도 - 그 회차의 후반부에 스파이크가 우연히 적었던 것으로 설명됩니다 — 즉 그 "수렴"도 실체가 아니었습니다. + 그 회차의 후반부에 스파이크가 우연히 적었던 것으로 설명됩니다, 즉 그 "수렴"도 실체가 아니었습니다. - **부하 가드 자체에 결함이 둘 있었고 고쳤습니다 (`run-bench.sh`)**: 1. `RN_BENCH_REQUIRE_QUIET=1` 이 `load_guard || true` 로 호출되어 **"refusing to run" 을 출력하고도 @@ -811,10 +811,10 @@ rather than a larger `WARMUP`. ## 8. To fill in the blanks 1. Android 실기기(Physical Device) 또는 수렴 기준 재검토 후 re-run → 에뮬레이터 단독 실행(WARMUP=100k, 400k)은 `NEVER SETTLED`로 인용 불가 판정됨(§7d 참조). 실기기 측정 또는 에뮬레이터 수렴 기준 조정을 통해 Android 열을 완성해야 함. -2. ~~`xcodebuild -downloadPlatform iOS` (several GB, internal disk — **ask first**)~~ **Done, and it +2. ~~`xcodebuild -downloadPlatform iOS` (several GB, internal disk, **ask first**)~~ **Done, and it needed none of that.** §7a: the platform download was never required, the block was a misread `-showdestinations`. iOS is filled in §4b and §4c. What is left on this side is a **quiet-machine - re-run** to clear the † on the ratio rows — boot the simulator, wait for a 1-minute load under + re-run** to clear the † on the ratio rows, boot the simulator, wait for a 1-minute load under ~2.0, `./run-bench.sh ios`, and check that the warmup verdict block says `Adequate` for both sweeps rather than only for the boundary one. 3. Startup: RN's time-to-first-render against the reference project's 237/480 ms and 4/28 ms. The @@ -860,13 +860,13 @@ repository and there is otherwise nothing to tell two runs of "the benchmark" ap - **Above a 1-minute load average of 4.0 the run prints a boxed warning into its own results file** saying the figures are not quotable. `RN_BENCH_REQUIRE_QUIET=1` turns that into a refusal. -- **A platform that cannot run writes a results file saying why**, rather than exiting. A missing +- **A platform that cannot run writes a results file saying why**: rather than exiting. A missing file is indistinguishable from a file nobody looked for, and that is how an empty column becomes a filled one in someone's memory. `all` therefore never stops early, and prints a summary of which platforms produced figures. - **Two emulators attached is an error, not a guess.** The app id is the same on both, so a two-device run installs over itself. `ANDROID_SERIAL=emulator-5554 ./run-bench.sh android` selects - one, and setting it is your assertion that that emulator is free — the script cannot tell. + one, and setting it is your assertion that that emulator is free, the script cannot tell. - **Progress lines are prefixed `#`** and printed as each row starts, because the heavy rows take tens of seconds and a benchmark that prints only at the end is indistinguishable from a hung one. - **The capture window is 900 s** (`RN_BENCH_CAPTURE_SECS`), with a watchdog, and the run says @@ -877,7 +877,7 @@ repository and there is otherwise nothing to tell two runs of "the benchmark" ap - **The iOS capture reads the log at `--level info`.** This is load-bearing, not a detail: `log stream` defaults to `--level default` and silently drops the Info-level messages React Native's `console.log` produces on iOS. Without it the capture is empty while the app runs - perfectly. §7b has the A/B that established it — 0 lines against 91 on a single launch. + perfectly. §7b has the A/B that established it, 0 lines against 91 on a single launch. **How long a full quiet-machine pass should take.** The `ios` row is now measured; the rest are still estimates, and the machine has been at load 2.4–6.9 during the iOS work: @@ -893,16 +893,16 @@ still estimates, and the machine has been at load 2.4–6.9 during the iOS work: **The `android` estimate is wrong by a factor of about 60, and the error is a unit slip.** The same 18 rows at the same counts took **~75 s on the iOS simulator**. The estimate's call counts were -right — `repeat` does pay the warmup on every repetition, so 5 x (100 000 + 10 000) = 550 000 calls -per row, and 5.5 M boundary calls is correct — but "550 000 of those is ~8 min" is not: 550 000 x +right, `repeat` does pay the warmup on every repetition, so 5 x (100 000 + 10 000) = 550 000 calls +per row, and 5.5 M boundary calls is correct, but "550 000 of those is ~8 min" is not: 550 000 x 14.3 µs is **7.9 seconds**, not 8 minutes. Microseconds were read as milliseconds. Checked against the measurement rather than merely re-divided: on iOS the boundary `sumArray(1000)` row is 550 000 x 64.6 µs = **35.5 s**, the two async round trips are **15.2 s** together, and the rest of the rows and both sweeps account for the remainder of the 75 s. Android still pays a JNI hop -per boundary call that iOS does not, so it will be slower than 75 s — by how much is **미측정**. +per boundary call that iOS does not, so it will be slower than 75 s, by how much is **미측정**. **So budget under 10 minutes for `./run-bench.sh all` on a quiet machine**, most of it builds rather than benchmarking. If it ever does prove too slow, the honest fix is to drop `sumArray(1000)` -to a smaller length and say so — **not** to lower the warmup, which is the one number §3a shows +to a smaller length and say so, **not** to lower the warmup, which is the one number §3a shows there is no room in. diff --git a/benchmarks/rn-benchmark/README.md b/benchmarks/rn-benchmark/README.md index 748898527..a6b4c8bf2 100644 --- a/benchmarks/rn-benchmark/README.md +++ b/benchmarks/rn-benchmark/README.md @@ -20,14 +20,14 @@ The comparison itself, including what corresponds to what and what is still unme ``` Every run writes to `results/-.txt` as well as to the terminal, **opening with -the conditions it was taken under** — load average before and after, device, engine version, +the conditions it was taken under**, load average before and after, device, engine version, reference-repo commit, a digest of the harness sources, and the warmup/iteration/repetition counts. A per-call figure without those is not a measurement. See `COMPARISON.md` §9. `android` and `ios` each require **exactly one** device: the app id is the same on all of them, so a two-device run would install over itself. Set `ANDROID_SERIAL` to choose when more than one is -attached — doing so is your assertion that that device is free, which the script cannot check. -`ios` will not boot a simulator for you — it expects one already booted, deliberately, because the +attached, doing so is your assertion that that device is free, which the script cannot check. +`ios` will not boot a simulator for you, it expects one already booted, deliberately, because the simulators on this machine are shared. A platform that cannot run **writes a results file saying why** and returns non-zero; `all` carries @@ -37,7 +37,7 @@ average of 4.0 every run prints a warning into its own output that its figures a | variable | default | | |---|---|---| -| `ANDROID_SERIAL` | — | pick one of several attached devices | +| `ANDROID_SERIAL` | - | pick one of several attached devices | | `RN_BENCH_REQUIRE_QUIET` | `0` | `1` = refuse to run on a busy machine | | `RN_BENCH_MAX_LOAD` | `4.0` | the threshold for the above | | `RN_BENCH_CAPTURE_SECS` | `900` | how long to wait for the on-device report | @@ -68,7 +68,7 @@ it in the same run: | `nativeLoopNs(...)` | the callee with the crossing removed, timed by the native clock | Plus, kept in their own block and never subtracted from anything, the native → JS direction as -callback and Promise **round trips** — see "the asymmetry" in `COMPARISON.md`. +callback and Promise **round trips**, see "the asymmetry" in `COMPARISON.md`. ### The method, and why it is copied rather than invented @@ -77,13 +77,13 @@ callback and Promise **round trips** — see "the asymmetry" in `COMPARISON.md`. boundary costs measured with two different loop shapes are not comparable, which is the whole point of this repository. What that buys, specifically: -- **Warmup of the same shape and the same block as the timed loop**, never a different block. +- **Warmup of the same shape and the same block as the timed loop**: never a different block. - **A warmup count taken from a measured convergence point.** The reference project settled on 100 000 after a 40-repetition sweep; `run-host-sweep.sh` is that same sweep, so the question can - be asked of Hermes rather than assumed. (It has an answer, and it is not the same as the JVM's — + be asked of Hermes rather than assumed. (It has an answer, and it is not the same as the JVM's, see `COMPARISON.md` §3.) - **The same sweep on device, over the boundary itself.** The host sweep prices Hermes, which has - no JIT. It cannot price the Kotlin callee, which runs on **ART, which does** — and which the + no JIT. It cannot price the Kotlin callee, which runs on **ART, which does**, and which the reference project measured as needing the largest warmup of any target it tested. So the on-device run sweeps `addInts` through the TurboModule, cold, before anything else, and every report ends with a verdict on whether its own warmup was large enough. `COMPARISON.md` §3a is @@ -93,7 +93,7 @@ of this repository. What that buys, specifically: - **Every baseline measured in the same process.** No row is compared against a number from another run, another build or another machine. - **No wall-clock assertion.** The only thing checked is that a row came back positive rather than - as a zero from a clock with no resolution — the failure that would otherwise read as "the + as a zero from a clock with no resolution, the failure that would otherwise read as "the boundary is free". --- @@ -156,19 +156,19 @@ it and used for that one command. | | state | |---|---| -| Android build | **green** — debug APK builds, JSI shim links into `libappmodules.so`, `BenchPackage` autolinked | -| Android run | **done but not quotable** — full report, 21 rows, on API 26 emulator. `results/android-20260814-203238.txt`, load 3.17; warmup verdict is INSUFFICIENT (both sweeps signal inadequate settling), so figures are marked NOT QUOTABLE. `COMPARISON.md` §7c | -| iOS `pod install` + codegen | **green** — 87 pods, `RNBenchSpec` generated | -| iOS module compile | **green** — `check-ios-module.sh`, with a negative control | -| iOS app build | **green** — `./build-ios.sh Release`, `** BUILD SUCCEEDED **`. The earlier "blocked" verdict was a misread `-showdestinations`; nothing was downloaded to clear it. `COMPARISON.md` §7a | -| iOS run | **done** — full report, 21 rows, on iPhone 17 Pro / iOS 26.2. `results/ios-20260814-200903.txt`, ~75 s | -| host JS rows + warmup sweep | **done** — see `COMPARISON.md` §3 | +| Android build | **green**, debug APK builds, JSI shim links into `libappmodules.so`, `BenchPackage` autolinked | +| Android run | **done but not quotable**, full report, 21 rows, on API 26 emulator. `results/android-20260814-203238.txt`, load 3.17; warmup verdict is INSUFFICIENT (both sweeps signal inadequate settling), so figures are marked NOT QUOTABLE. `COMPARISON.md` §7c | +| iOS `pod install` + codegen | **green**, 87 pods, `RNBenchSpec` generated | +| iOS module compile | **green**, `check-ios-module.sh`, with a negative control | +| iOS app build | **green**, `./build-ios.sh Release`, `** BUILD SUCCEEDED **`. The earlier "blocked" verdict was a misread `-showdestinations`; nothing was downloaded to clear it. `COMPARISON.md` §7a | +| iOS run | **done**, full report, 21 rows, on iPhone 17 Pro / iOS 26.2. `results/ios-20260814-200903.txt`, ~75 s | +| host JS rows + warmup sweep | **done**, see `COMPARISON.md` §3 | | on-device warmup sweep | **done on iOS** (boundary settles at 10 000 calls, 10x margin); **attempted on Android** but did not settle within ±5% over 40 repetitions (first 806 ns against plateau 308 ns = 2.62x, result not conclusive). §3a | Two things to know before reading or re-running any of this, both written up in `COMPARISON.md` §7: - **The iOS capture needs `log stream --level info`.** Without it the capture is empty while the app - runs the whole benchmark perfectly — `console.log` reaches the unified log at Info level, which + runs the whole benchmark perfectly, `console.log` reaches the unified log at Info level, which `log stream` drops by default. This cost two runs that exited 0 with empty results files. -- **An empty or incomplete capture is now a non-zero exit**, and every results file ends with a +- **An empty or incomplete capture is now a non-zero exit**: and every results file ends with a `capture check` block. A run that produced no rows can no longer look like a measurement. diff --git a/benchmarks/rn-benchmark/RNBench/README.md b/benchmarks/rn-benchmark/RNBench/README.md index 3e2c3f850..834ac71d9 100644 --- a/benchmarks/rn-benchmark/RNBench/README.md +++ b/benchmarks/rn-benchmark/RNBench/README.md @@ -60,13 +60,13 @@ yarn ios If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. -This is one way to run your app — you can also build it directly from Android Studio or Xcode. +This is one way to run your app, you can also build it directly from Android Studio or Xcode. ## Step 3: Modify your app Now that you have successfully run the app, let's make changes! -Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). +Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes, this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: diff --git a/benchmarks/typedpython/py/binary_trees.py b/benchmarks/typedpython/py/binary_trees.py index 9ab44c379..43e5ea17e 100644 --- a/benchmarks/typedpython/py/binary_trees.py +++ b/benchmarks/typedpython/py/binary_trees.py @@ -2,6 +2,7 @@ class Node: + __slots__ = ("left", "right") left: "Node | None" right: "Node | None" diff --git a/benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py b/benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py new file mode 100644 index 000000000..a0cd59c56 --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py @@ -0,0 +1,21 @@ +# typedpython: compiled +import array + + +def dot_arr(a: array.array[float], b: array.array[float]) -> float: + s: float = 0.0 + for i in range(len(a)): + s += a[i] * b[i] + return s + + +def scale_arr(a: array.array[float], k: float) -> None: + for i in range(len(a)): + a[i] = a[i] * k + + +def dot_list(a: list[float], b: list[float]) -> float: + s: float = 0.0 + for i in range(len(a)): + s += a[i] * b[i] + return s diff --git a/benchmarks/typedpython/results/2026-10-17-buffer-atomic-vs-plain.txt b/benchmarks/typedpython/results/2026-10-17-buffer-atomic-vs-plain.txt new file mode 100644 index 000000000..2e3a10391 --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-buffer-atomic-vs-plain.txt @@ -0,0 +1,4 @@ +# atomic vs plain buffer element access, 2026-10-17, quiet window (end load 1.15), interleaved 11 pairs, n=2,000,000 doubles, CPython 3.15.0rc1t +compiled: ['dot_arr', 'dot_list', 'scale_arr'] +{'dot': 1.001, 'scale': 2.539} (atomic / plain, median of 11) +dot_arr atomic 1.94 ms, plain 1.92 ms, dot_list (copy-in) 12.28 ms diff --git a/benchmarks/typedpython/results/2026-10-17-eval-breaker-ab.json b/benchmarks/typedpython/results/2026-10-17-eval-breaker-ab.json new file mode 100644 index 000000000..8c46e092c --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-eval-breaker-ab.json @@ -0,0 +1,308 @@ +{ + "fannkuch-redux": { + "size": "10", + "pairs": [ + { + "before_s": 0.617363250348717, + "after_s": 0.7355743343941867, + "cpython_s": 6.021780250128359, + "after_over_before": 1.191477357906708, + "speedup_before": 9.75403094811193, + "speedup_after": 8.186501307291872, + "load": [ + 3.6943359375, + 3.02001953125, + 2.7109375 + ] + }, + { + "after_s": 0.27955787535756826, + "before_s": 0.2578067500144243, + "cpython_s": 6.004611459095031, + "after_over_before": 1.0843698830303201, + "speedup_before": 23.291133605924095, + "speedup_after": 21.47895655386148, + "load": [ + 3.513671875, + 3.00244140625, + 2.7080078125 + ] + }, + { + "before_s": 0.2493240418843925, + "after_s": 0.2699953746050596, + "cpython_s": 5.923735792282969, + "after_over_before": 1.0829095042918166, + "speedup_before": 23.759184022171873, + "speedup_after": 21.940138052172244, + "load": [ + 3.31201171875, + 2.96875, + 2.69775390625 + ] + }, + { + "after_s": 0.2705523748882115, + "before_s": 0.2492111660540104, + "cpython_s": 5.963486583903432, + "after_over_before": 1.0856350426512427, + "speedup_before": 23.929451791140824, + "speedup_after": 22.04189331683916, + "load": [ + 3.12646484375, + 2.93603515625, + 2.6875 + ] + }, + { + "before_s": 0.2592446249909699, + "after_s": 0.2709223749116063, + "cpython_s": 5.9388052499853075, + "after_over_before": 1.045045292341329, + "speedup_before": 22.908113331924124, + "speedup_after": 21.9206894665786, + "load": [ + 2.9560546875, + 2.90380859375, + 2.67724609375 + ] + }, + { + "after_s": 0.27158462489023805, + "before_s": 0.25109495781362057, + "cpython_s": 5.959664166904986, + "after_over_before": 1.0816012685201997, + "speedup_before": 23.73470267502801, + "speedup_after": 21.944041085954726, + "load": [ + 3.12255859375, + 2.93994140625, + 2.69189453125 + ] + }, + { + "before_s": 0.2600955842062831, + "after_s": 0.2711727498099208, + "cpython_s": 5.943080165889114, + "after_over_before": 1.0425888260942267, + "speedup_before": 22.84960040373322, + "speedup_after": 21.916214553471654, + "load": [ + 2.9521484375, + 2.90771484375, + 2.681640625 + ] + }, + { + "after_s": 0.2703690002672374, + "before_s": 0.2523987921886146, + "cpython_s": 5.931899290997535, + "after_over_before": 1.0711976785736514, + "speedup_before": 23.50209063823371, + "speedup_after": 21.940012668369313, + "load": [ + 2.79541015625, + 2.8759765625, + 2.67138671875 + ] + }, + { + "before_s": 0.25141441682353616, + "after_s": 0.2719767093658447, + "cpython_s": 5.947182500269264, + "after_over_before": 1.0817864496479568, + "speedup_before": 23.654898455737715, + "speedup_after": 21.86651391634243, + "load": [ + 2.8134765625, + 2.87939453125, + 2.67431640625 + ] + }, + { + "after_s": 0.27185420878231525, + "before_s": 0.24923187540844083, + "cpython_s": 5.944646291900426, + "after_over_before": 1.0907682186991572, + "speedup_before": 23.851869999205956, + "speedup_after": 21.86703791906546, + "load": [ + 4.029296875, + 3.13037109375, + 2.763671875 + ] + }, + { + "before_s": 0.2507732920348644, + "after_s": 0.2709160838276148, + "cpython_s": 5.93707116600126, + "after_over_before": 1.080322715506522, + "speedup_before": 23.67505374207012, + "speedup_after": 21.91479768244047, + "load": [ + 3.86669921875, + 3.111328125, + 2.7587890625 + ] + } + ], + "median_after_over_before": 1.0817864496479568, + "min_after_over_before": 1.0425888260942267, + "max_after_over_before": 1.191477357906708, + "median_speedup_before": 23.654898455737715, + "median_speedup_after": 21.916214553471654 + }, + "nbody": { + "size": "1000000", + "pairs": [ + { + "before_s": 0.5934657920151949, + "after_s": 0.5233573750592768, + "cpython_s": 4.006437666714191, + "after_over_before": 0.8818661194980502, + "speedup_before": 6.750915925768493, + "speedup_after": 7.655261696198342, + "load": [ + 3.95703125, + 3.142578125, + 2.771484375 + ] + }, + { + "after_s": 0.13168670795857906, + "before_s": 0.13310000021010637, + "cpython_s": 4.033577458932996, + "after_over_before": 0.9893817261510418, + "speedup_before": 30.3048644069553, + "speedup_after": 30.63010323108482, + "load": [ + 3.73583984375, + 3.12109375, + 2.767578125 + ] + }, + { + "before_s": 0.12832820834591985, + "after_s": 0.1279506660066545, + "cpython_s": 4.010861457791179, + "after_over_before": 0.9970579941531822, + "speedup_before": 31.25471406083652, + "speedup_after": 31.346936932571975, + "load": [ + 3.73583984375, + 3.12109375, + 2.767578125 + ] + }, + { + "after_s": 0.12794629205018282, + "before_s": 0.12665566708892584, + "cpython_s": 4.075236166827381, + "after_over_before": 1.0101900293206054, + "speedup_before": 32.175711205769645, + "speedup_after": 31.851147083097963, + "load": [ + 3.6767578125, + 3.11865234375, + 2.7685546875 + ] + }, + { + "before_s": 0.13051274977624416, + "after_s": 0.1298941671848297, + "cpython_s": 4.128146333154291, + "after_over_before": 0.9952603665735725, + "speedup_before": 31.630214980771886, + "speedup_after": 31.780844533844594, + "load": [ + 3.46240234375, + 3.0830078125, + 2.7578125 + ] + }, + { + "after_s": 0.1337217092514038, + "before_s": 0.1343525000847876, + "cpython_s": 4.176525415852666, + "after_over_before": 0.9953049564914258, + "speedup_before": 31.086324506182844, + "speedup_after": 31.23296463404143, + "load": [ + 3.42529296875, + 3.08154296875, + 2.7587890625 + ] + }, + { + "before_s": 0.13416387513279915, + "after_s": 0.1304794168099761, + "cpython_s": 4.760525666177273, + "after_over_before": 0.9725376274412463, + "speedup_before": 35.48291715236439, + "speedup_after": 36.48487847788492, + "load": [ + 3.31103515625, + 3.0634765625, + 2.75390625 + ] + }, + { + "after_s": 0.14065329218283296, + "before_s": 0.13487516716122627, + "cpython_s": 4.0361085827462375, + "after_over_before": 1.0428405402063352, + "speedup_before": 29.9247716810729, + "speedup_after": 28.695443384999226, + "load": [ + 3.44580078125, + 3.09521484375, + 2.7666015625 + ] + }, + { + "before_s": 0.12785816565155983, + "after_s": 0.1291002919897437, + "cpython_s": 4.002894583158195, + "after_over_before": 1.0097148768860718, + "speedup_before": 31.307304955922156, + "speedup_after": 31.006084660724103, + "load": [ + 3.32958984375, + 3.07666015625, + 2.76171875 + ] + }, + { + "after_s": 0.12757845874875784, + "before_s": 0.12731604231521487, + "cpython_s": 4.101447000168264, + "after_over_before": 1.0020611419328702, + "speedup_before": 32.214691295647675, + "speedup_after": 32.14842882092897, + "load": [ + 3.142578125, + 3.0419921875, + 2.7509765625 + ] + }, + { + "before_s": 0.13188975024968386, + "after_s": 0.12789770774543285, + "cpython_s": 4.141773249953985, + "after_over_before": 0.9697319731313953, + "speedup_before": 31.403298907709573, + "speedup_after": 32.383483042540185, + "load": [ + 3.142578125, + 3.0419921875, + 2.7509765625 + ] + } + ], + "median_after_over_before": 0.9953049564914258, + "min_after_over_before": 0.8818661194980502, + "max_after_over_before": 1.0428405402063352, + "median_speedup_before": 31.307304955922156, + "median_speedup_after": 31.346936932571975 + } +} diff --git a/benchmarks/typedpython/results/2026-10-17-eval-breaker-nopoll-experiment.json b/benchmarks/typedpython/results/2026-10-17-eval-breaker-nopoll-experiment.json new file mode 100644 index 000000000..957de61fb --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-eval-breaker-nopoll-experiment.json @@ -0,0 +1,155 @@ +{ + "fannkuch-redux": { + "size": "10", + "pairs": [ + { + "before_s": 0.5521271657198668, + "after_s": 0.6070619588717818, + "cpython_s": 5.987047249916941, + "after_over_before": 1.099496631505698, + "speedup_before": 10.84360202076091, + "speedup_after": 9.86233309865735, + "load": [ + 2.44775390625, + 2.82666015625, + 2.69140625 + ] + }, + { + "after_s": 0.2503165830858052, + "before_s": 0.250257957726717, + "cpython_s": 6.000943332910538, + "after_over_before": 1.0002342597199334, + "speedup_before": 23.979031026312455, + "speedup_after": 23.97341502082383, + "load": [ + 2.41162109375, + 2.8125, + 2.68701171875 + ] + }, + { + "before_s": 0.26244004210457206, + "after_s": 0.2621465837582946, + "cpython_s": 5.951287917327136, + "after_over_before": 0.9988818080353739, + "speedup_before": 22.67675263882095, + "speedup_after": 22.702137987097956, + "load": [ + 2.50146484375, + 2.818359375, + 2.68994140625 + ] + }, + { + "after_s": 0.25116583285853267, + "before_s": 0.2525885826908052, + "cpython_s": 5.937470917124301, + "after_over_before": 0.9943673232688663, + "speedup_before": 23.506489699070784, + "speedup_after": 23.639644172734826, + "load": [ + 2.380859375, + 2.7880859375, + 2.6796875 + ] + }, + { + "before_s": 0.2509259171783924, + "after_s": 0.2509960001334548, + "cpython_s": 5.959116999991238, + "after_over_before": 1.000279297395225, + "speedup_before": 23.748511381367926, + "speedup_after": 23.741880336032327, + "load": [ + 2.27001953125, + 2.75830078125, + 2.66943359375 + ] + }, + { + "after_s": 0.24977354193106294, + "before_s": 0.24995683319866657, + "cpython_s": 5.941518500447273, + "after_over_before": 0.9992667083141593, + "speedup_before": 23.770178332051973, + "speedup_after": 23.78762159719512, + "load": [ + 2.248046875, + 2.74560546875, + 2.6650390625 + ] + }, + { + "before_s": 0.24905870808288455, + "after_s": 0.24908804101869464, + "cpython_s": 5.942821917124093, + "after_over_before": 1.0001177751865649, + "speedup_before": 23.861128819259648, + "speedup_after": 23.85831890129992, + "load": [ + 2.203125, + 2.72021484375, + 2.65673828125 + ] + }, + { + "after_s": 0.2507365830242634, + "before_s": 0.24994562473148108, + "cpython_s": 5.947943000122905, + "after_over_before": 1.0031645214579452, + "speedup_before": 23.796947862211372, + "speedup_after": 23.72187946561963, + "load": [ + 2.1064453125, + 2.69140625, + 2.64697265625 + ] + }, + { + "before_s": 0.2490944997407496, + "after_s": 0.24957312503829598, + "cpython_s": 5.9209016668610275, + "after_over_before": 1.0019214607229165, + "speedup_before": 23.769700547476287, + "speedup_after": 23.7241155911819, + "load": [ + 2.09765625, + 2.6796875, + 2.64306640625 + ] + }, + { + "after_s": 0.24998520780354738, + "before_s": 0.2506584580987692, + "cpython_s": 5.945883291773498, + "after_over_before": 0.9973140731003918, + "speedup_before": 23.721055881667432, + "speedup_after": 23.784940493143544, + "load": [ + 2.001953125, + 2.64013671875, + 2.62939453125 + ] + }, + { + "before_s": 0.2553401249460876, + "after_s": 0.25701129203662276, + "cpython_s": 6.079530125018209, + "after_over_before": 1.0065448667376036, + "speedup_before": 23.809536892416883, + "speedup_after": 23.654719902936826, + "load": [ + 1.92138671875, + 2.61279296875, + 2.61962890625 + ] + } + ], + "median_after_over_before": 1.0002342597199334, + "min_after_over_before": 0.9943673232688663, + "max_after_over_before": 1.099496631505698, + "median_speedup_before": 23.769700547476287, + "median_speedup_after": 23.7241155911819 + } +} diff --git a/benchmarks/typedpython/results/2026-10-17-fast-entry-ab.json b/benchmarks/typedpython/results/2026-10-17-fast-entry-ab.json new file mode 100644 index 000000000..f7fb1f35b --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-fast-entry-ab.json @@ -0,0 +1,314 @@ +{ + "spectral-norm": { + "size": "800", + "pairs": [ + { + "before_s": 1.1141414581798017, + "after_s": 0.798894583247602, + "cpython_s": 3.3166386247612536, + "after_over_before": 0.7170495069385303, + "speedup_before": 2.976855946263522, + "speedup_after": 4.151534751029004, + "load": [ + 2.32373046875, + 2.8388671875, + 3.1533203125 + ] + }, + { + "after_s": 0.05642041703686118, + "before_s": 0.5941873327828944, + "cpython_s": 3.3151200828142464, + "after_over_before": 0.09495392096733946, + "speedup_before": 5.579250683934612, + "speedup_after": 58.757454427328625, + "load": [ + 2.21728515625, + 2.80810546875, + 3.140625 + ] + }, + { + "before_s": 0.5943655422888696, + "after_s": 0.05575770791620016, + "cpython_s": 3.283662290778011, + "after_over_before": 0.09381046502372974, + "speedup_before": 5.524651173641064, + "speedup_after": 58.89162975840255, + "load": [ + 2.19970703125, + 2.79443359375, + 3.1337890625 + ] + }, + { + "after_s": 0.05573229165747762, + "before_s": 0.5970102911815047, + "cpython_s": 3.3194454996846616, + "after_over_before": 0.09335231315222627, + "speedup_before": 5.560114371086234, + "speedup_after": 59.560542029842956, + "load": [ + 2.19970703125, + 2.79443359375, + 3.1337890625 + ] + }, + { + "before_s": 0.5956689999438822, + "after_s": 0.05633941572159529, + "cpython_s": 3.339143875055015, + "after_over_before": 0.0945817488016046, + "speedup_before": 5.605703629649343, + "speedup_after": 59.26834405872438, + "load": [ + 2.103515625, + 2.76416015625, + 3.12109375 + ] + }, + { + "after_s": 0.056803958024829626, + "before_s": 0.6093089170753956, + "cpython_s": 3.3818732076324522, + "after_over_before": 0.09322686150316217, + "speedup_before": 5.550342548513829, + "speedup_after": 59.53587259102964, + "load": [ + 2.25537109375, + 2.78466796875, + 3.1259765625 + ] + }, + { + "before_s": 0.607568624895066, + "after_s": 0.05638662492856383, + "cpython_s": 3.3777862917631865, + "after_over_before": 0.09280700585600918, + "speedup_before": 5.5595140258379345, + "speedup_after": 59.904033909504264, + "load": [ + 2.31494140625, + 2.7880859375, + 3.125 + ] + }, + { + "after_s": 0.05656562466174364, + "before_s": 0.6080640838481486, + "cpython_s": 3.3865731661207974, + "after_over_before": 0.09302576186339882, + "speedup_before": 5.569434630456687, + "speedup_after": 59.86980938285648, + "load": [ + 2.28955078125, + 2.77490234375, + 3.1181640625 + ] + }, + { + "before_s": 0.6106662922538817, + "after_s": 0.059617833234369755, + "cpython_s": 3.3802520409226418, + "after_over_before": 0.09762751602733612, + "speedup_before": 5.5353506224268845, + "speedup_after": 56.6986731576471, + "load": [ + 2.28955078125, + 2.77490234375, + 3.1181640625 + ] + }, + { + "after_s": 0.05642887484282255, + "before_s": 0.6073171659372747, + "cpython_s": 3.380930291954428, + "after_over_before": 0.09291500060884278, + "speedup_before": 5.566992803071236, + "speedup_after": 59.91489820365369, + "load": [ + 2.26611328125, + 2.76171875, + 3.111328125 + ] + }, + { + "before_s": 0.6068534590303898, + "after_s": 0.05705862492322922, + "cpython_s": 3.3896440002135932, + "after_over_before": 0.09402372858580321, + "speedup_before": 5.5856054699423705, + "speedup_after": 59.406338739748186, + "load": [ + 2.32470703125, + 2.765625, + 3.1103515625 + ] + } + ], + "median_after_over_before": 0.09381046502372974, + "min_after_over_before": 0.09280700585600918, + "max_after_over_before": 0.7170495069385303, + "median_speedup_before": 5.560114371086234, + "median_speedup_after": 59.406338739748186 + }, + "nbody": { + "skipped": [ + "n/a (nothing compiled)", + "n/a (nothing compiled)" + ] + }, + "fannkuch-redux": { + "size": "10", + "pairs": [ + { + "before_s": 0.7067045411095023, + "after_s": 0.63105074968189, + "cpython_s": 6.030535667203367, + "after_over_before": 0.8929484855030952, + "speedup_before": 8.533319536528843, + "speedup_after": 9.55634023134167, + "load": [ + 2.53857421875, + 2.80224609375, + 3.12109375 + ] + }, + { + "after_s": 0.2739174999296665, + "before_s": 0.27865454088896513, + "cpython_s": 6.0477433749474585, + "after_over_before": 0.9830003094721281, + "speedup_before": 21.703372769931963, + "speedup_after": 22.078703903548806, + "load": [ + 2.5751953125, + 2.80517578125, + 3.1201171875 + ] + }, + { + "before_s": 0.27599437488242984, + "after_s": 0.2764261248521507, + "cpython_s": 6.042619833257049, + "after_over_before": 1.0015643433671602, + "speedup_before": 21.893996338987453, + "speedup_after": 21.859800105684677, + "load": [ + 2.486328125, + 2.7783203125, + 3.1064453125 + ] + }, + { + "after_s": 0.2808002079837024, + "before_s": 0.2761829160153866, + "cpython_s": 6.013481875415891, + "after_over_before": 1.0167182388937432, + "speedup_before": 21.77354762624373, + "speedup_after": 21.41551788225496, + "load": [ + 2.447265625, + 2.76513671875, + 3.099609375 + ] + }, + { + "before_s": 0.2764340410940349, + "after_s": 0.2888324582017958, + "cpython_s": 6.036349332891405, + "after_over_before": 1.0448512674440964, + "speedup_before": 21.83649057475527, + "speedup_after": 20.899137757827916, + "load": [ + 2.8115234375, + 2.8349609375, + 3.1220703125 + ] + }, + { + "after_s": 0.2795829582028091, + "before_s": 0.2770982920192182, + "cpython_s": 6.064032624941319, + "after_over_before": 1.0089667322215705, + "speedup_before": 21.884049088692134, + "speedup_after": 21.6895645711799, + "load": [ + 2.76611328125, + 2.82373046875, + 3.1142578125 + ] + }, + { + "before_s": 0.27542241709306836, + "after_s": 0.2786844172514975, + "cpython_s": 6.044994332827628, + "after_over_before": 1.011843626211903, + "speedup_before": 21.948083952748682, + "speedup_after": 21.691181704545574, + "load": [ + 3.18505859375, + 2.9091796875, + 3.142578125 + ] + }, + { + "after_s": 0.27875441685318947, + "before_s": 0.27709112502634525, + "cpython_s": 6.081121041905135, + "after_over_before": 1.006002688922953, + "speedup_before": 21.946285870133714, + "speedup_after": 21.81533519918307, + "load": [ + 3.169921875, + 2.91064453125, + 3.1416015625 + ] + }, + { + "before_s": 0.277795874979347, + "after_s": 0.2738394159823656, + "cpython_s": 6.0339908339083195, + "after_over_before": 0.9857576754972495, + "speedup_before": 21.7209518836697, + "speedup_after": 22.034778347237236, + "load": [ + 3.2109375, + 2.92919921875, + 3.1455078125 + ] + }, + { + "after_s": 0.2780231246724725, + "before_s": 0.2764884582720697, + "cpython_s": 6.0378861669451, + "after_over_before": 1.0055505622549086, + "speedup_before": 21.83775121999382, + "speedup_after": 21.717208502199533, + "load": [ + 3.27392578125, + 2.94677734375, + 3.150390625 + ] + }, + { + "before_s": 0.2783553749322891, + "after_s": 0.2733089169487357, + "cpython_s": 6.045158042106777, + "after_over_before": 0.9818704489367918, + "speedup_before": 21.717410858609366, + "speedup_after": 22.11840765971299, + "load": [ + 3.572265625, + 3.013671875, + 3.1728515625 + ] + } + ], + "median_after_over_before": 1.0055505622549086, + "min_after_over_before": 0.8929484855030952, + "max_after_over_before": 1.0448512674440964, + "median_speedup_before": 21.83649057475527, + "median_speedup_after": 21.717208502199533 + } +} diff --git a/benchmarks/typedpython/results/2026-10-17-interval-proofs-ab.json b/benchmarks/typedpython/results/2026-10-17-interval-proofs-ab.json new file mode 100644 index 000000000..790a0ccda --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-interval-proofs-ab.json @@ -0,0 +1,308 @@ +{ + "fannkuch-redux": { + "size": "10", + "pairs": [ + { + "before_s": 1.0887560830451548, + "after_s": 0.6829974167048931, + "cpython_s": 5.911647166591138, + "after_over_before": 0.627319036229501, + "speedup_before": 5.429725958505584, + "speedup_after": 8.655445865537468, + "load": [ + 2.66748046875, + 2.986328125, + 3.36767578125 + ] + }, + { + "after_s": 0.27054166700690985, + "before_s": 0.2709987089037895, + "cpython_s": 5.913880208041519, + "after_over_before": 0.9983134905006432, + "speedup_before": 21.822540158820743, + "speedup_after": 21.8594062551203, + "load": [ + 2.49072265625, + 2.93701171875, + 3.34521484375 + ] + }, + { + "before_s": 0.2726931250654161, + "after_s": 0.27129745902493596, + "cpython_s": 5.907416833098978, + "after_over_before": 0.9948819170261615, + "speedup_before": 21.663240801108767, + "speedup_after": 21.774685447960664, + "load": [ + 2.37109375, + 2.90478515625, + 3.3310546875 + ] + }, + { + "after_s": 0.270517417229712, + "before_s": 0.2704989998601377, + "cpython_s": 5.924571041949093, + "after_over_before": 1.0000680866457319, + "speedup_before": 21.90237688498815, + "speedup_after": 21.900885727140434, + "load": [ + 2.16015625, + 2.841796875, + 3.3037109375 + ] + }, + { + "before_s": 0.27097137505188584, + "after_s": 0.27024612482637167, + "cpython_s": 5.908709957730025, + "after_over_before": 0.9973235171966954, + "speedup_before": 21.805661046664504, + "speedup_after": 21.864180148841232, + "load": [ + 2.14697265625, + 2.82763671875, + 3.2958984375 + ] + }, + { + "after_s": 0.27170295920223, + "before_s": 0.2769245416857302, + "cpython_s": 6.009743249975145, + "after_over_before": 0.9811443852115281, + "speedup_before": 21.701735835299655, + "speedup_after": 22.118799396299767, + "load": [ + 2.0546875, + 2.796875, + 3.2822265625 + ] + }, + { + "before_s": 0.2953220000490546, + "after_s": 0.2890004999935627, + "cpython_s": 6.02235220791772, + "after_over_before": 0.9785945508480849, + "speedup_before": 20.3924943177866, + "speedup_after": 20.83855290233707, + "load": [ + 2.21044921875, + 2.81640625, + 3.2861328125 + ] + }, + { + "after_s": 0.27058391692116857, + "before_s": 0.2701001246459782, + "cpython_s": 6.634010416921228, + "after_over_before": 1.0017911590223236, + "speedup_before": 24.561300834705143, + "speedup_after": 24.51738629703542, + "load": [ + 2.3251953125, + 2.82177734375, + 3.2822265625 + ] + }, + { + "before_s": 0.3240792923606932, + "after_s": 0.3907488747499883, + "cpython_s": 6.6358414171263576, + "after_over_before": 1.205719970269169, + "speedup_before": 20.475980951417316, + "speedup_after": 16.982368589985445, + "load": [ + 2.69921875, + 2.89111328125, + 3.3037109375 + ] + }, + { + "after_s": 0.2735611251555383, + "before_s": 0.2766624162904918, + "cpython_s": 6.041726541239768, + "after_over_before": 0.9887903417582489, + "speedup_before": 21.83790130313919, + "speedup_after": 22.085471895192093, + "load": [ + 2.95947265625, + 2.94287109375, + 3.3173828125 + ] + }, + { + "before_s": 0.2883912082761526, + "after_s": 0.28800000017508864, + "cpython_s": 7.008296665735543, + "after_over_before": 0.9986434811816823, + "speedup_before": 24.301353386004266, + "speedup_after": 24.334363407898863, + "load": [ + 2.88232421875, + 2.9267578125, + 3.3095703125 + ] + } + ], + "median_after_over_before": 0.9973235171966954, + "min_after_over_before": 0.627319036229501, + "max_after_over_before": 1.205719970269169, + "median_speedup_before": 21.805661046664504, + "median_speedup_after": 21.864180148841232 + }, + "binary-trees": { + "size": "16", + "pairs": [ + { + "before_s": 3.155360875185579, + "after_s": 3.0856687501072884, + "cpython_s": 3.590506291948259, + "after_over_before": 0.9779131047651747, + "speedup_before": 1.1379067035358, + "speedup_after": 1.1636071732662223, + "load": [ + 2.7314453125, + 2.89453125, + 3.2958984375 + ] + }, + { + "after_s": 2.393941333051771, + "before_s": 2.388220750261098, + "cpython_s": 3.54007404204458, + "after_over_before": 1.0023953325043538, + "speedup_before": 1.4823060396144505, + "speedup_after": 1.478763908358494, + "load": [ + 2.8525390625, + 2.9140625, + 3.2978515625 + ] + }, + { + "before_s": 2.403391916770488, + "after_s": 2.392654124647379, + "cpython_s": 3.593896792270243, + "after_over_before": 0.9955322342360468, + "speedup_before": 1.4953436296396774, + "speedup_after": 1.5020544571187862, + "load": [ + 2.88134765625, + 2.91650390625, + 3.2939453125 + ] + }, + { + "after_s": 2.379018457606435, + "before_s": 2.754461499862373, + "cpython_s": 3.6588334999978542, + "after_over_before": 0.8636963913727975, + "speedup_before": 1.3283298750701973, + "speedup_after": 1.5379592740440777, + "load": [ + 3.04638671875, + 2.951171875, + 3.3017578125 + ] + }, + { + "before_s": 2.3003429169766605, + "after_s": 2.309331167023629, + "cpython_s": 3.513636375311762, + "after_over_before": 1.0039073522389357, + "speedup_before": 1.5274402565725864, + "speedup_after": 1.5214952387449465, + "load": [ + 3.580078125, + 3.0673828125, + 3.3388671875 + ] + }, + { + "after_s": 2.2887648749165237, + "before_s": 2.2567570419050753, + "cpython_s": 3.2699287086725235, + "after_over_before": 1.0141831098417349, + "speedup_before": 1.4489502626796564, + "speedup_after": 1.4286870374973686, + "load": [ + 3.693359375, + 3.09912109375, + 3.34814453125 + ] + }, + { + "before_s": 2.160762708168477, + "after_s": 2.1336489161476493, + "cpython_s": 3.3091259165666997, + "after_over_before": 0.987451749366866, + "speedup_before": 1.5314619713016095, + "speedup_after": 1.5509233461620295, + "load": [ + 3.740234375, + 3.12841796875, + 3.35498046875 + ] + }, + { + "after_s": 2.1829029163345695, + "before_s": 2.155100916977972, + "cpython_s": 3.1836033752188087, + "after_over_before": 1.012900555671232, + "speedup_before": 1.4772409728650073, + "speedup_after": 1.4584264611110465, + "load": [ + 3.5205078125, + 3.0927734375, + 3.3408203125 + ] + }, + { + "before_s": 2.9248738340102136, + "after_s": 2.181852374691516, + "cpython_s": 3.206505249720067, + "after_over_before": 0.7459646119846608, + "speedup_before": 1.0962883979592775, + "speedup_after": 1.4696252078802639, + "load": [ + 3.2060546875, + 3.03955078125, + 3.31884765625 + ] + }, + { + "after_s": 2.146172667387873, + "before_s": 2.1432514172047377, + "cpython_s": 3.184945832937956, + "after_over_before": 1.0013629993008217, + "speedup_before": 1.4860346328812009, + "speedup_after": 1.4840119256641096, + "load": [ + 3.669921875, + 3.13818359375, + 3.35205078125 + ] + }, + { + "before_s": 2.1659594592638314, + "after_s": 2.5923933750018477, + "cpython_s": 3.8596382504329085, + "after_over_before": 1.196879915694707, + "speedup_before": 1.7819531357917135, + "speedup_after": 1.4888320143273617, + "load": [ + 3.56640625, + 3.13330078125, + 3.34716796875 + ] + } + ], + "median_after_over_before": 1.0013629993008217, + "min_after_over_before": 0.7459646119846608, + "max_after_over_before": 1.196879915694707, + "median_speedup_before": 1.4823060396144505, + "median_speedup_after": 1.4840119256641096 + } +} diff --git a/benchmarks/typedpython/results/2026-10-17-wordfreq-cost-rule-ab.json b/benchmarks/typedpython/results/2026-10-17-wordfreq-cost-rule-ab.json new file mode 100644 index 000000000..32019a960 --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-wordfreq-cost-rule-ab.json @@ -0,0 +1,155 @@ +{ + "wordfreq": { + "size": "3000000", + "pairs": [ + { + "before_s": 2.534866082947701, + "after_s": 2.4698741249740124, + "cpython_s": 2.246576082892716, + "after_over_before": 0.9743607923073743, + "speedup_before": 0.8862701260653014, + "speedup_after": 0.9095913270140251, + "load": [ + 2.22265625, + 2.5498046875, + 3.80224609375 + ] + }, + { + "after_s": 2.054086625110358, + "before_s": 2.0703248330391943, + "cpython_s": 2.226471666712314, + "after_over_before": 0.9921566859124232, + "speedup_before": 1.0754214175385701, + "speedup_after": 1.0839229658061253, + "load": [ + 2.1943359375, + 2.53173828125, + 3.78076171875 + ] + }, + { + "before_s": 2.0203522499650717, + "after_s": 2.0748487496748567, + "cpython_s": 2.2474577920511365, + "after_over_before": 1.0269737614867542, + "speedup_before": 1.112408884188384, + "speedup_after": 1.083191144609133, + "load": [ + 2.0986328125, + 2.505859375, + 3.76416015625 + ] + }, + { + "after_s": 2.057975041680038, + "before_s": 2.072377833072096, + "cpython_s": 2.225645957980305, + "after_over_before": 0.9930501131781035, + "speedup_before": 1.0739576164453584, + "speedup_after": 1.081473736514991, + "load": [ + 2.17041015625, + 2.513671875, + 3.75927734375 + ] + }, + { + "before_s": 2.0653403750620782, + "after_s": 2.0860109999775887, + "cpython_s": 2.24187570810318, + "after_over_before": 1.0100083381727765, + "speedup_before": 1.0854751764758366, + "speedup_after": 1.0747190250325938, + "load": [ + 2.39697265625, + 2.5546875, + 3.76611328125 + ] + }, + { + "after_s": 2.047571041621268, + "before_s": 2.0459034591913223, + "cpython_s": 2.240035249851644, + "after_over_before": 1.0008150836357668, + "speedup_before": 1.0948880504542748, + "speedup_after": 1.093996351930228, + "load": [ + 2.3291015625, + 2.53564453125, + 3.74462890625 + ] + }, + { + "before_s": 2.064202000387013, + "after_s": 2.066363917198032, + "cpython_s": 2.2770254579372704, + "after_over_before": 1.0010473378141354, + "speedup_before": 1.1031020498528514, + "speedup_after": 1.1019479381080624, + "load": [ + 2.22265625, + 2.509765625, + 3.728515625 + ] + }, + { + "after_s": 2.047510417178273, + "before_s": 2.029783667065203, + "cpython_s": 2.238690916914493, + "after_over_before": 1.0087333199102446, + "speedup_before": 1.1029209433689757, + "speedup_after": 1.093372174389076, + "load": [ + 2.12451171875, + 2.484375, + 3.71240234375 + ] + }, + { + "before_s": 2.051607625093311, + "after_s": 2.0582683752290905, + "cpython_s": 2.2774744578637183, + "after_over_before": 1.0032466003997604, + "speedup_before": 1.1100926073815574, + "speedup_after": 1.1065002432494886, + "load": [ + 1.951171875, + 2.43505859375, + 3.68017578125 + ] + }, + { + "after_s": 2.0976750422269106, + "before_s": 2.0433855829760432, + "cpython_s": 2.259623875375837, + "after_over_before": 1.0265683871429683, + "speedup_before": 1.1058235382501125, + "speedup_after": 1.0772039662430268, + "load": [ + 2.03515625, + 2.4443359375, + 3.67578125 + ] + }, + { + "before_s": 2.05512237502262, + "after_s": 2.072784666903317, + "cpython_s": 2.2494797920808196, + "after_over_before": 1.0085942774480778, + "speedup_before": 1.0945721867565479, + "speedup_after": 1.08524528765522, + "load": [ + 1.9521484375, + 2.419921875, + 3.65966796875 + ] + } + ], + "median_after_over_before": 1.0032466003997604, + "min_after_over_before": 0.9743607923073743, + "max_after_over_before": 1.0269737614867542, + "median_speedup_before": 1.0948880504542748, + "median_speedup_after": 1.0839229658061253 + } +} diff --git a/benchmarks/typedpython/results/2026-10-17-wordfreq-object-mode.md b/benchmarks/typedpython/results/2026-10-17-wordfreq-object-mode.md new file mode 100644 index 000000000..519d6cfbe --- /dev/null +++ b/benchmarks/typedpython/results/2026-10-17-wordfreq-object-mode.md @@ -0,0 +1,20 @@ +# wordfreq and object-only compiled code (#150, 2026-10-17) + +Quiet windows, interleaved against develop, 11 pairs, CPython 3.15.0rc1t. + +| build | after / before (median, range) | vs CPython | +|---|---|---| +| #150 without the cost rule (`word_of` and `main` compiled) | 1.335 (1.314 to 1.767) | 1.1x to **0.8x** | +| #150 with the cost rule (only `next_state` compiled, as on develop) | 1.003 (0.974 to 1.027) | 1.1x to 1.1x | + +Split, measured in one process: `word_of` compiled 248.6 ms against 95.7 ms interpreted (2.6x slower); +`main` 332 ms against 345 ms. + +Cause (read in the generated C): in an impure function an `int` is a CPython object, so each loop +iteration boxes constants, calls generic `PyNumber_*` / `PyObject_RichCompare`, and looks up the +global `chr` through the module and builtins dicts. CPython 3.15's specialising interpreter +(`BINARY_OP_ADD_INT`, `LOAD_GLOBAL_BUILTIN` caches, the small-int cache) runs that faster. + +Consequence: the cost rule (`typedpython/cost.py`) keeps an impure function interpreted when it has +loops and none of them does native work. The real fix, native ints in impure functions with in-place +promotion on overflow, pre-boxed constants and cached builtins, is #232. diff --git a/benchmarks/typedpython/run.py b/benchmarks/typedpython/run.py index 288cfd746..104ad572d 100644 --- a/benchmarks/typedpython/run.py +++ b/benchmarks/typedpython/run.py @@ -12,6 +12,10 @@ interpreter with the gate's dependencies, e.g. that module's .venv): each benchmark is copied with the `# typedpython: compiled` marker, compiled through `typedpython.pipeline`, and its `main()` is run from the built extension. Without the option the column reports n/a. + +binary-trees declares `__slots__` (TypedPython compiles fixed-layout classes only, SPEC N-11). The +speedup is reported against that CPython version; `CPython (no __slots__)` runs the same program +with the `__slots__` line removed, so what adding the slots changes in CPython is recorded too. """ import argparse import datetime @@ -40,7 +44,10 @@ "binary-trees": ("binary_trees.py", "binary_trees.mjs", "BinaryTrees", "binary_trees", "10", "16"), "wordfreq": ("wordfreq.py", "wordfreq.mjs", "Wordfreq", "wordfreq", "100000", "3000000"), } -COLUMNS = ["CPython", "Node", "Java", "Rust", "TypedPython @compiled"] +NOSLOTS = "CPython (no __slots__)" +COLUMNS = ["CPython", NOSLOTS, "Node", "Java", "Rust", "TypedPython @compiled"] +SLOTS_LINE = ' __slots__ = ("left", "right")\n' +NOSLOTS_BENCHMARKS = {"binary-trees"} NOT_BUILT = "n/a (not built)" @@ -99,6 +106,18 @@ def build_compiled(tp_dir: Path, tp_python: str) -> dict[str, tuple[str, list[st return built +def noslots_source(name: str) -> Path: + """The benchmark with its `__slots__` line removed, written under tp-build/noslots/.""" + py = BENCHMARKS[name][0] + source = (ROOT / "py" / py).read_text() + if SLOTS_LINE not in source: + sys.exit(f"{name}: expected the line {SLOTS_LINE.strip()!r} in py/{py}") + out = TP_BUILD / "noslots" / py + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(source.replace(SLOTS_LINE, "", 1)) + return out + + def commands(name: str, size: str, python: str, java_source: bool, compiled: dict[str, tuple[str, list[str]] | str] | None = None) -> dict[str, list[str]]: py, js, cls, rs, _, _ = BENCHMARKS[name] @@ -110,12 +129,22 @@ def commands(name: str, size: str, python: str, java_source: bool, "Java": java, "Rust": [str(TARGET / "release" / rs), size], } + if name in NOSLOTS_BENCHMARKS: + cmds[NOSLOTS] = [python, str(noslots_source(name)), size] entry = (compiled or {}).get(name) if isinstance(entry, tuple): cmds["TypedPython @compiled"] = [python, "-c", RUN_SNIPPET, Path(py).stem, entry[0], size] return cmds +def source_commit() -> str: + """The commit the harness (and the TypedPython sources beside it) was measured at.""" + result = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "HEAD"], capture_output=True, text=True) + dirty = subprocess.run(["git", "-C", str(ROOT), "status", "--porcelain", "--untracked-files=no"], + capture_output=True, text=True).stdout.strip() + return result.stdout.strip() + ("+dirty" if dirty else "") if result.returncode == 0 else "unknown" + + def run_once(cmd: list[str]) -> tuple[float, str]: start = time.perf_counter() result = subprocess.run(cmd, capture_output=True, text=True) @@ -179,7 +208,8 @@ def measure(args: argparse.Namespace) -> int: print("| benchmark | size | " + " | ".join(COLUMNS) + " |") print("|---|---|" + "---|" * len(COLUMNS)) for name, row in results.items(): - cells = [f"{row[c]['median_s']:.3f} s" if isinstance(row.get(c), dict) else str(row.get(c, NOT_BUILT)) + cells = [f"{row[c]['median_s']:.3f} s" if isinstance(row.get(c), dict) + else str(row.get(c, ", " if c == NOSLOTS else NOT_BUILT)) for c in COLUMNS] # type: ignore[index] flag = "" if row["outputs_agree"] else " (OUTPUTS DIFFER)" print(f"| {name}{flag} | {row['size']} | " + " | ".join(cells) + " |") @@ -190,6 +220,7 @@ def measure(args: argparse.Namespace) -> int: out_dir.mkdir(exist_ok=True) path = out_dir / f"{datetime.date.today().isoformat()}-{socket.gethostname()}.json" payload = {"date": datetime.datetime.now().isoformat(timespec="seconds"), + "commit": source_commit(), "host": socket.gethostname(), "load_average": load, "repeat": args.repeat, "python": subprocess.run([args.python, "--version"], capture_output=True, text=True).stdout.strip(), "results": results} @@ -216,6 +247,7 @@ def interleaved(args: argparse.Namespace) -> int: size = args.size.get(name, spec[5]) cmds = commands(name, size, args.python, False, compiled) a_cmd, b_cmd = cmds["CPython"], cmds["TypedPython @compiled"] + c_cmd = cmds.get(NOSLOTS) pairs = [] for i in range(args.pairs): order = (a_cmd, b_cmd) if i % 2 == 0 else (b_cmd, a_cmd) # alternate which runs first @@ -225,11 +257,21 @@ def interleaved(args: argparse.Namespace) -> int: if o_a != o_b: print(f"{name}: OUTPUTS DIFFER in pair {i}: {o_a!r} vs {o_b!r}") return 1 - pairs.append({"cpython_s": t_a, "compiled_s": t_b, "ratio": t_a / t_b, "load": load}) + pair = {"cpython_s": t_a, "compiled_s": t_b, "ratio": t_a / t_b, "load": load} + if c_cmd is not None: + t_c, o_c = run_once(c_cmd) + if o_c != o_a: + print(f"{name}: OUTPUTS DIFFER in pair {i} (no __slots__): {o_c!r} vs {o_a!r}") + return 1 + pair.update(cpython_noslots_s=t_c, ratio_vs_noslots=t_c / t_b) + pairs.append(pair) ratios = [p["ratio"] for p in pairs] results[name] = {"size": size, "compiled_functions": entry[1], "pairs": pairs, "median_ratio": statistics.median(ratios), "min_ratio": min(ratios), "max_ratio": max(ratios)} + if c_cmd is not None: + results[name]["median_ratio_vs_noslots"] = statistics.median( + p["ratio_vs_noslots"] for p in pairs) print(f"measured {name}", file=sys.stderr) print() @@ -237,7 +279,7 @@ def interleaved(args: argparse.Namespace) -> int: print("|---|---|---|---|---|") for name, r in results.items(): if "skipped" in r: - print(f"| {name} | — | {r['skipped']} | | |") + print(f"| {name} | - | {r['skipped']} | | |") continue loads = [p["load"][0] for p in r["pairs"]] # type: ignore[index] print(f"| {name} | {r['size']} | {r['median_ratio']:.1f}x | {r['min_ratio']:.1f}–{r['max_ratio']:.1f}x " @@ -248,6 +290,7 @@ def interleaved(args: argparse.Namespace) -> int: out_dir.mkdir(exist_ok=True) path = out_dir / f"{datetime.date.today().isoformat()}-{socket.gethostname()}-interleaved.json" path.write_text(json.dumps({"date": datetime.datetime.now().isoformat(timespec="seconds"), + "commit": source_commit(), "host": socket.gethostname(), "method": "interleaved A/B", "pairs": args.pairs, "results": results}, indent=2) + "\n") print(f"wrote {path}") diff --git a/binary/aarch64-linux-android.tar.xz b/binary/aarch64-linux-android.tar.xz deleted file mode 100644 index 4ff6017ea..000000000 Binary files a/binary/aarch64-linux-android.tar.xz and /dev/null differ diff --git a/binary/arm64-iphoneos.zip b/binary/arm64-iphoneos.zip deleted file mode 100644 index 1182d7a7a..000000000 Binary files a/binary/arm64-iphoneos.zip and /dev/null differ diff --git a/binary/arm64-iphonesimulator.zip b/binary/arm64-iphonesimulator.zip deleted file mode 100644 index ffb316901..000000000 Binary files a/binary/arm64-iphonesimulator.zip and /dev/null differ diff --git a/binary/combine_simulator.sh b/binary/combine_simulator.sh deleted file mode 100644 index 0ae4540e7..000000000 --- a/binary/combine_simulator.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Unzip archives -unzip -o -qq "./arm64-iphoneos.zip" -d "./arm64-iphoneos" -unzip -o -qq "./arm64-iphonesimulator.zip" -d "./arm64-iphonesimulator" -unzip -o -qq "./x86_64-iphonesimulator.zip" -d "./x86_64-iphonesimulator" - -# Set module directory -ARM_DIR="./arm64-iphonesimulator/" -x86_DIR="path/to/modules" - -# Loop through each module in the directory -for module in "$MODULE_DIR"/*; do - # Extract the module name without the architecture suffix - module_name=$(basename "$module" | sed 's/_x86_64//;s/_arm64//') - - # Define the paths for x86_64 and arm64 versions of the module - x86_64_module="$MODULE_DIR/${module_name}_x86_64/module.dylib" - arm64_module="$MODULE_DIR/${module_name}_arm64/module.dylib" - - # Define the output path for the combined module - output_module="$MODULE_DIR/${module_name}/module.dylib" - - # Create the combined module using lipo - lipo -create -output "$output_module" "$x86_64_module" "$arm64_module" -done diff --git a/binary/cpython-3.13.0+20241008-aarch64-apple-darwin-pgo+lto-full.tar.zst b/binary/cpython-3.13.0+20241008-aarch64-apple-darwin-pgo+lto-full.tar.zst deleted file mode 100644 index a99f6508a..000000000 Binary files a/binary/cpython-3.13.0+20241008-aarch64-apple-darwin-pgo+lto-full.tar.zst and /dev/null differ diff --git a/binary/cpython-3.13.0+20241008-x86_64-apple-darwin-pgo+lto-full.tar.zst b/binary/cpython-3.13.0+20241008-x86_64-apple-darwin-pgo+lto-full.tar.zst deleted file mode 100644 index 6915f3f51..000000000 Binary files a/binary/cpython-3.13.0+20241008-x86_64-apple-darwin-pgo+lto-full.tar.zst and /dev/null differ diff --git a/binary/cpython-3.13.0+20241008-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst b/binary/cpython-3.13.0+20241008-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst deleted file mode 100644 index 00922a019..000000000 Binary files a/binary/cpython-3.13.0+20241008-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst and /dev/null differ diff --git a/binary/cpython-3.13.0+20241008-x86_64_v4-unknown-linux-gnu-lto-full.tar.zst b/binary/cpython-3.13.0+20241008-x86_64_v4-unknown-linux-gnu-lto-full.tar.zst deleted file mode 100644 index e0d1e8b23..000000000 Binary files a/binary/cpython-3.13.0+20241008-x86_64_v4-unknown-linux-gnu-lto-full.tar.zst and /dev/null differ diff --git a/binary/download.md b/binary/download.md deleted file mode 100644 index 510fdb136..000000000 --- a/binary/download.md +++ /dev/null @@ -1 +0,0 @@ -These stand-alone python binaries were downloaded from https://github.com/astral-sh/python-build-standalone/releases/tag/20241008 diff --git a/binary/x86_64-iphonesimulator.zip b/binary/x86_64-iphonesimulator.zip deleted file mode 100644 index 688c97ec8..000000000 Binary files a/binary/x86_64-iphonesimulator.zip and /dev/null differ diff --git a/binary/x86_64-linux-android.tar.xz b/binary/x86_64-linux-android.tar.xz deleted file mode 100644 index 9b42e72ae..000000000 Binary files a/binary/x86_64-linux-android.tar.xz and /dev/null differ diff --git a/docs/archive/README.md b/docs/archive/README.md index 0659efaab..c8ffdf8be 100644 --- a/docs/archive/README.md +++ b/docs/archive/README.md @@ -24,7 +24,7 @@ full text is in git (`git log -- docs/design/upcall-design.md`). | [`wasm-design-experiment-log.md`](wasm-design-experiment-log.md) | [`../platforms/wasm-design.md`](../platforms/wasm-design.md) | The chronological lab notebook of the wasm experiment, including its "parked" phase and the conclusions that were wrong before they were tested. wasm is now integrated (`tools/wasm/build-cpython.sh`, `wasmJsTest`). | | [`wasm-youtrack-issue.md`](wasm-youtrack-issue.md) | [`../platforms/wasm-design.md`](../platforms/wasm-design.md) | A draft Kotlin/Wasm compiler issue. Do not file it: Kotlin 2.4.20-Beta2 imports the shared memory, so the problem it describes no longer applies. | -## Investigation narratives — `investigations/` +## Investigation narratives, `investigations/` The current file of the same name in [`../investigations/`](../investigations/) keeps the conclusion, the numbers with their conditions, and how to reproduce them. The archived copy is the full original, diff --git a/docs/archive/android-ffm-design.md b/docs/archive/android-ffm-design.md index 5c1e10f12..649c05dab 100644 --- a/docs/archive/android-ffm-design.md +++ b/docs/archive/android-ffm-design.md @@ -2,7 +2,7 @@ > [`python-multiplatform/src/androidMain/README.md`](../../python-multiplatform/src/androidMain/README.md) on 2026-10-03; kept for history. > This was a plan to replace the Kotlin/Native JNI bridge on Android with a PanamaPort-style FFM path > (ART method patching, hidden-API bypass, 14 NDK trampolines). It was **not** built: PanamaPort is -> not a dependency (AGENTS.md §12.9 — licence, and dependence on ART-internal struct layouts, see §2–§3 +> not a dependency (AGENTS.md §12.9, licence, and dependence on ART-internal struct layouts, see §2–§3 > below), and Android binds CPython through JNI `RegisterNatives` with the calling convention chosen > per function and per API level (measured; SPEC C-3). The Kotlin/Native Android library > (`libmultiplatform_python3.14.so`) is still the bridge. The 14-shape census in §6 is still @@ -106,7 +106,7 @@ Based on an analysis of the ~330 functions in `EmbedAPI.kt`, there are exactly 1 ### Upcalls **No.** CPython upcalls (callbacks) also have a small set of signature shapes (e.g., `PyCFunction` is `II_I`, `destructor` is `I_V`, `getter` is `II_I`, `newfunc` is `III_I`, etc.), which collapse into just 4 distinct shapes (`I_V`, `I_I`, `II_I`, `III_I`), perfectly overlapping with the downcall trampolines. -The real reason LLVM is unnecessary for upcalls is not just the small number of shapes, but that **routing happens through data, not through the function pointer**. Every standard callback receives enough context to identify the Kotlin target—usually the `PyObject *self` instance data (e.g., `destructor`, `reprfunc`), the `PyTypeObject *type` (e.g., `newfunc`), or the `void *closure` (e.g., `getter`, `setter`). Because the identity of the target is carried in the arguments, a handful of shared static trampolines can serve unlimited Kotlin targets. This is exactly how PyO3, JPype, and JEP work. +The real reason LLVM is unnecessary for upcalls is not just the small number of shapes, but that **routing happens through data, not through the function pointer**. Every standard callback receives enough context to identify the Kotlin target, usually the `PyObject *self` instance data (e.g., `destructor`, `reprfunc`), the `PyTypeObject *type` (e.g., `newfunc`), or the `void *closure` (e.g., `getter`, `setter`). Because the identity of the target is carried in the arguments, a handful of shared static trampolines can serve unlimited Kotlin targets. This is exactly how PyO3, JPype, and JEP work. The ONLY case that would require runtime code generation (LLVM or similar) is handing a bare Kotlin function pointer, with no context argument, to a third-party C API (like `qsort`). CPython's object model does not do this. @@ -159,7 +159,7 @@ That has two consequences, both of which cut against work already committed here Native Image works under a closed-world assumption, so runtime reflection does not work unless every reflective access is registered at build time. This rules out the obvious way of exposing Kotlin to -Python — walking classes reflectively at run time — and is why that discussion landed on a +Python, walking classes reflectively at run time, and is why that discussion landed on a **build-time generated class/function table**, reached through a single upcall entry point. The registration policy decided there is worth carrying forward: every `public` declaration is @@ -179,7 +179,7 @@ Two problems from that discussion remain unresolved: `desktopMain/.../Panama.kt` reaches `java.lang.foreign` and `jdk.incubator.foreign` entirely through `Class.forName` plus `MethodHandles`, precisely so it compiles on any JDK. Under Native Image that is a problem twice over: the reflective lookups need build-time registration, and more -fundamentally **GraalVM requires FFM downcalls themselves to be registered at compile time** — +fundamentally **GraalVM requires FFM downcalls themselves to be registered at compile time**, an unregistered one fails at run time with `ForeignFunctionsRuntime$UnregisteredForeignStubException: Cannot perform downcall ... as it was not registered at compilation time`. diff --git a/docs/archive/downcall-design-android-first-diagnosis.md b/docs/archive/downcall-design-android-first-diagnosis.md index 38b98df65..5eae58324 100644 --- a/docs/archive/downcall-design-android-first-diagnosis.md +++ b/docs/archive/downcall-design-android-first-diagnosis.md @@ -1,9 +1,9 @@ # 다운콜 설계 초안의 Android 진단 (2026-08 초) > **Superseded** by [`../design/downcall-design.md`](../design/downcall-design.md) on 2026-10-03; kept for history. -> 아래 "확인된 버그" 는 **수정되었다** — Android 는 `RegisterNatives` 로 전부 바인딩한다 +> 아래 "확인된 버그" 는 **수정되었다**, Android 는 `RegisterNatives` 로 전부 바인딩한다 > (`python-multiplatform/src/androidMain/README.md`, ROADMAP §2). 호출 규약 표의 전환 비용(115/35/25 ns)은 -> 외부 벤치마크에서 가져온 값이며, 이 프로젝트가 실측한 값(API 26-36)과 반대 경향이다 — 실측은 본 문서의 +> 외부 벤치마크에서 가져온 값이며, 이 프로젝트가 실측한 값(API 26-36)과 반대 경향이다, 실측은 본 문서의 > "Measured" 절과 `androidMain/README.md` 에 있다. ### 확인된 버그: 현재 Android JNI 배선이 어긋나 있다 @@ -42,5 +42,5 @@ shape 트램폴린은 정적이고 `Long`/`Double` 만 주고받으므로 `@Crit **애노테이션을 붙이는 것이 성능 개선인 동시에 위 버그의 수정**이다. `@CriticalNative` 는 API 34 부터 공식 SDK 에 포함된다. minSdk 26 구간에서는 `RegisterNatives` 로 -등록해야 한다 — `RegisterNatives` 는 호출 규약이 아니라 바인딩 수단이므로 둘은 대안 관계가 아니다. +등록해야 한다, `RegisterNatives` 는 호출 규약이 아니라 바인딩 수단이므로 둘은 대안 관계가 아니다. diff --git a/docs/archive/downcall-design-benchmark-drift.md b/docs/archive/downcall-design-benchmark-drift.md index ff60c1755..0ee0e941a 100644 --- a/docs/archive/downcall-design-benchmark-drift.md +++ b/docs/archive/downcall-design-benchmark-drift.md @@ -22,19 +22,19 @@ is a direct comparison, not a re-derivation): | Platform | ratio recorded in `upcall-design.md` | ratio, this pass (3 runs) | consistent? | |---|---|---|---| -| desktop | 2.49–2.89x | 2.13–2.34x | **no — see below** | +| desktop | 2.49–2.89x | 2.13–2.34x | **no, see below** | | iOS simulator | 1.33–1.43x | 1.38–1.42x | yes, nested inside the old range | -| wasmJs | 2.56–3.36x | 2.36–2.63x | mostly — top of the new range overlaps the bottom of the old one. **Superseded: since drifted to 1.58–1.68x, pinned to `4472f83a` and explained — see "wasmJs: closed too" below** | +| wasmJs | 2.56–3.36x | 2.36–2.63x | mostly, top of the new range overlaps the bottom of the old one. **Superseded: since drifted to 1.58–1.68x, pinned to `4472f83a` and explained, see "wasmJs: closed too" below** | **Desktop's ratio moved outside the previously recorded range, and it is the upcall side that moved, not the downcall side.** This pass's downcall-same-shape figure (315.69–335.32 ns) sits at -the bottom of the old 315–527 ns range — consistent, just at its floor. This pass's upcall figure +the bottom of the old 315–527 ns range, consistent, just at its floor. This pass's upcall figure (687.83–741.26 ns, read from the same `UpcallBoundaryCostTest` runs) sits *below* the old range's floor of 861 ns entirely. So on this machine, at this commit (`65e1bf5d`), a desktop upcall through `ctypes` measures cheaper than what `upcall-design.md`'s table recorded, while the downcall side did not move. Two things separate the two tables and either could be the cause: `develop` has moved a great deal since that table was captured (this session's own `git merge` pulled in 87 changed files, -including upcall-adjacent ones — see the merge log above), and the two tables were also captured on +including upcall-adjacent ones, see the merge log above), and the two tables were also captured on different runs of the same machine, which is exactly the kind of thing this document elsewhere warns is not comparable. Neither is ruled out here; this is reported as an open discrepancy, not resolved into either explanation. @@ -43,8 +43,8 @@ into either explanation. The discrepancy above was tested directly rather than argued from a second reading. `537c1a0b` (the commit the 861 ns floor was recorded on) and the current tip were each built in their own -freshly created `git worktree` — not reused, not incrementally recompiled — and -`:python-multiplatform:desktopTest` (the full suite, not a filtered subset — see the pitfall below) +freshly created `git worktree`, not reused, not incrementally recompiled, and +`:python-multiplatform:desktopTest` (the full suite, not a filtered subset, see the pitfall below) was run three times against each, clearing `build/test-results` before every run: | | run 1 | run 2 | run 3 | @@ -61,16 +61,16 @@ exactly, which rules out a stale or miscalibrated re-measurement on this end. **A methodology pitfall found along the way:** an initial attempt to bisect the ~75 commits in between used `--tests UpcallBoundaryCostTest` to skip the other ~359 tests and go faster. That filter alone moved the number into the 1300–1400 ns range at *every* commit tried, old and new -alike — it does not merely add noise, it changes which regime the measurement lands in. Re-running +alike, it does not merely add noise, it changes which regime the measurement lands in. Re-running the same checkouts with the unfiltered full suite reproduced the fast band again. The cause is below; the practical lesson is that this benchmark's absolute number is not a property of the timed code alone, so a comparison across two runs is only valid if both used the same suite scope -this document already required (Validation runs the full suite) — a filtered re-run of one +this document already required (Validation runs the full suite), a filtered re-run of one benchmark test is not equivalent to the run it is being compared against, even on the same commit. **Bisection was attempted and did not converge to one commit; it converged to a window, and to a mechanism.** Git bisect (full-suite grading, threshold at 750 ns) was run between `537c1a0b` and -the fast tip. Partway through, machine load reached 10–12 on this 8-core host — two Android +the fast tip. Partway through, machine load reached 10–12 on this 8-core host, two Android emulators and other concurrent agent activity were running, unrelated to this task and outside its control (`ps aux` at the time confirmed it; this document's own environment notes elsewhere warn that this machine hosts several worktrees' worth of parallel agent work). Under that load, repeated @@ -78,8 +78,8 @@ measurements of the *same* checkout spanned both regimes (one commit read 672, 7 across three consecutive runs), which made single-commit attribution unreliable. What did hold up under repetition was the window: commits at or before `4722cfe9` measured consistently in the slow band (854–855 ns), and commits at or after `ec8ff389` measured consistently in the fast band (672, -700 ns, each confirmed in its own fresh clean worktree). That window — -`4722cfe9..ec8ff389` (~15 commits) — contains both commits this task flagged as candidates: +700 ns, each confirmed in its own fresh clean worktree). That window, +`4722cfe9..ec8ff389` (~15 commits), contains both commits this task flagged as candidates: `6776329d` ("Perf: Price the layer users actually call, and stop a module read raising an exception per access") and `d45071e7` (proxy handle lifetime). `975d3900` sits well outside this window and is ruled out. @@ -88,27 +88,27 @@ ruled out. `git diff ..` for both `6776329d` and `d45071e7` shows changes confined to `PythonProxySource` (the generated-proxy source emitter, its tests) and, for `d45071e7`, a doc-only comment added to `HandleTable.kt`. Neither touches `UpcallTable.resolve`, `HandleTable.resolve`/ -`release`, the non-suspend branch of `UpcallTrampoline.invoke`, or desktop's `UpcallStub` — the +`release`, the non-suspend branch of `UpcallTrampoline.invoke`, or desktop's `UpcallStub`, the actual call chain `UpcallBoundaryCostTest` times through `TrampolineFragment`. A commit that does not touch the timed path cannot have made the timed path itself cheaper. What it can do is change what runs *before* the timed path in the same process. `desktopTest` sets no `forkEvery`, so Gradle's default applies: all ~360 tests in the suite run in one forked JVM, one JIT compiler, one heap (confirmed by reading the `tasks.named("desktopTest")` block in -`build.gradle.kts` — no fork-per-test config exists). `6776329d`'s own commit title is "stop a -module read raising an exception per access" — replacing an exception-driven control-flow path +`build.gradle.kts`, no fork-per-test config exists). `6776329d`'s own commit title is "stop a +module read raising an exception per access", replacing an exception-driven control-flow path (expensive on the JVM: every throw captures a stack trace) with a direct one. `GeneratedProxyCostTest`, touched by the same commit, drives `N = 10_000` iterations times 3 kept times multiple rows of generated-proxy calls that exercise exactly that module-attribute path, and it runs earlier in the same shared JVM process as `UpcallBoundaryCostTest`. Removing tens of thousands of exception throws from an earlier test changes the shared process's GC pressure and -JIT tiering history by the time a later, unrelated benchmark in the same run gets timed — without +JIT tiering history by the time a later, unrelated benchmark in the same run gets timed, without the later benchmark's own code path changing at all. That is a plausible, mechanism-level explanation for why only desktop moved: iOS runs through XCTest's own process model and wasmJs through a Node process with a different (V8) JIT, neither sharing HotSpot's C2 warm-up state the way every desktop test in one Gradle-forked JVM does. -**This is not proven to the level of a single blamed commit** — machine contention prevented that — +**This is not proven to the level of a single blamed commit**, machine contention prevented that, but it is proven to the level that matters for the table: the number moved for a real, reproducible, repository-located reason, not for noise, and the most likely mechanism is a same-process warm-up artifact of the benchmark's own methodology rather than a genuine drop in the upcall @@ -118,16 +118,16 @@ boundary's per-call cost. The open discrepancy above is closed on that basis, no on the current tip (same machine, same session, two runs: 321.32 ns and 315.89 ns, tight) puts wasmJs's upcall figure well below the 687–741 ns-equivalent range implied by this table's own "this pass" ratio (2.36–2.63x against a ~192–205 ns downcall figure that still matches today's -run) — today's ratio is 1.26–1.67x. So the "yes, mostly consistent" verdict recorded for wasmJs +run), today's ratio is 1.26–1.67x. So the "yes, mostly consistent" verdict recorded for wasmJs above was accurate for the commit it was measured on (`65e1bf5d`) and has since gone stale in the same direction as desktop's did. iOS was not re-checked: this task's constraints rule out using the iOS simulator, so its "yes, nested inside the old range" verdict above stands unverified rather than reconfirmed. -### wasmJs: closed too — one commit, and the mechanism demonstrated rather than argued (2026-08-14) +### wasmJs: closed too, one commit, and the mechanism demonstrated rather than argued (2026-08-14) wasmJs's drift is **repository drift, not noise**, it is pinned to the **single commit `4472f83a`**, -and — unlike desktop's, which stayed at the level of a plausible story — its mechanism was +and, unlike desktop's, which stayed at the level of a plausible story, its mechanism was reproduced by a controlled experiment. It is *not* a genuine change in what the wasm upcall boundary costs. @@ -135,7 +135,7 @@ The measurement protocol was the one the desktop section above used, with two ch this machine: runs at the two ends were **interleaved** rather than run in two blocks, and the timed task was re-run with `--rerun` on `wasmJsNodeTest` alone rather than `--rerun-tasks`, so a full Kotlin recompile does not finish moments before the benchmark starts. Every run is the **whole -wasm suite** (filtering changes the measured region — see the pitfall below), `build/test-results` +wasm suite** (filtering changes the measured region, see the pitfall below), `build/test-results` is cleared before each, and all of them are 344/0/0 (258/0/0 at `537c1a0b`, whose suite was smaller). | commit | upcall | downcall, same shape | ratio | runs | @@ -149,18 +149,18 @@ is cleared before each, and all of them are 344/0/0 (258/0/0 at `537c1a0b`, whos | current tip | 355.82–373.21 ns | 222.07–226.88 ns | 1.58–1.64x | 4 (+4 earlier) | `4472f83a` and its own parent are adjacent commits measured minutes apart in one worktree, and their -upcall bands do not touch. **The downcall column does not move anywhere in this table** — it is the +upcall bands do not touch. **The downcall column does not move anywhere in this table**, it is the upcall side alone, exactly as on desktop. **The window does *not* overlap desktop's.** Desktop's was `4722cfe9..ec8ff389`; both of those endpoints measure in wasm's *slow* band and are indistinguishable from each other, so wasm did not move across desktop's window at all. `4472f83a` sits after `ec8ff389`. The two platforms therefore -do **not** share a cause in `commonMain`'s marshalling or handle paths — which was the hypothesis +do **not** share a cause in `commonMain`'s marshalling or handle paths, which was the hypothesis worth testing, and it is refuted rather than left open. **The MEMFS change is not the cause either.** `3355851c` (stdlib zip installed into MEMFS instead of resolving out of a CPython source checkout) was the obvious environmental confound, since it -lands after the desktop window. `73f2af6b` is its immediate parent and already measures 312–322 ns — +lands after the desktop window. `73f2af6b` is its immediate parent and already measures 312–322 ns, fully in the fast band. The transition happened before it. **Why a commit that only adds a branch to the timed function made it measure cheaper.** @@ -169,14 +169,14 @@ can only cost more, not less. What it also did was make `PythonProxySource.insta wasm for the first time. Before it, `GeneratedProxyCostTest` and `ProxyHandleLifetimeTest` printed "no proxies are installable on this target, so there is nothing to measure" and drove **zero** upcalls; after it, `GeneratedProxyCostTest` drives on the order of 270 000 calls -(`N = 10 000` × 3 kept × ~9 rows) straight through `UpcallEntry.invokeMethod` — earlier in the +(`N = 10 000` × 3 kept × ~9 rows) straight through `UpcallEntry.invokeMethod`, earlier in the **same Node process**, and therefore into the same V8 wasm tier-up state that `UpcallBoundaryCostTest` is later timed in. The benchmark's own 3 000-iteration warmup does not reach that state on its own. That was tested, not just asserted. At the **current tip**, with nothing changed but -`GeneratedProxyCostTest`'s body short-circuited so it installs nothing and calls nothing — the -pre-`4472f83a` behaviour, same commit, same build, suite still 344/0/0 — the upcall figure went +`GeneratedProxyCostTest`'s body short-circuited so it installs nothing and calls nothing, the +pre-`4472f83a` behaviour, same commit, same build, suite still 344/0/0, the upcall figure went back up: | current tip | upcall | downcall | ratio | @@ -186,8 +186,8 @@ back up: One lever, at one commit, moves the number across the whole gap and lands it back on `65e1bf5d`'s band (2.53–2.61x) and on this document's own "this pass" reading (2.36–2.63x). So the desktop -section's suspicion — that this benchmark reports a same-process warm-up artifact rather than a -boundary cost — is **confirmed on wasmJs by direct experiment**, on a different commit and a +section's suspicion, that this benchmark reports a same-process warm-up artifact rather than a +boundary cost, is **confirmed on wasmJs by direct experiment**, on a different commit and a non-overlapping window from desktop's. What the two platforms share is the methodology, not a code path. @@ -221,9 +221,9 @@ would have re-derived the same non-number. This section changes it and shows the #### What is warming, decided by experiment rather than by plausibility -Both earlier sections reached for "JIT tiering" as a story. It is testable, and the obvious rival — +Both earlier sections reached for "JIT tiering" as a story. It is testable, and the obvious rival, CPython-side state, meaning the specializing interpreter, free lists, the allocator, string -interning — makes an opposite prediction about one row that the report already prints. +interning, makes an opposite prediction about one row that the report already prints. The timed loops were run **40 times in succession** inside one run, on both hosts, in two suite configurations. Per 10 000 calls: @@ -243,7 +243,7 @@ what the number should be. **Two rows in the same sweep do not move at all**, and they are what identifies the mechanism: the empty Python loop (8–9 ns desktop, 14–16 ns wasm) and the pure-Python callee (24–27 ns, 47–54 ns) are flat from the very first rep, in every configuration, on both hosts. CPython-side state would move -those — they are pure interpreter work — and it does not. Only rows that cross into host code move. +those, they are pure interpreter work, and it does not. Only rows that cross into host code move. It is host JIT tier-up: C2 on the JVM, V8's tiering on wasm. The confirmation is on desktop, where the two runtimes can be told apart: CPython is a **native @@ -254,12 +254,12 @@ pure-Python rows are identical (8.44–8.59 ns) whether the class runs alone or Convergence needed 40 000 calls cold and 70 000 warm on desktop, and ~70 000 on wasm. The warm side settles later, so it sets the requirement; 100 000 is that with margin. The Kotlin-driven rows were -on the same curve and worse off — they warmed `N / 4` = 2 500, and desktop's downcall row reads 653, -266, 266, 161 before settling at ~141 — so they use the same count now. Warming the Python-driven and +on the same curve and worse off, they warmed `N / 4` = 2 500, and desktop's downcall row reads 653, +266, 266, 161 before settling at ~141, so they use the same count now. Warming the Python-driven and Kotlin-driven halves differently would make the headline ratio a ratio of two compilation states. `GeneratedProxyCostTest` needed far less, and the reason is its shape: it warms all 19 rows before -timing any, so the shared `_pm_invoke` path already received 19 × 3 000 = 57 000 calls — just under +timing any, so the shared `_pm_invoke` path already received 19 × 3 000 = 57 000 calls, just under the knee. 5 000 puts it at 95 000. Measured, its raw rows moved from 3–7% solo-vs-suite disagreement to 1–3.4%. @@ -273,9 +273,9 @@ recorded so it is not tried again. | | full suite | proxy test short-circuited | this class alone (`--tests`) | |---|---|---|---| -| desktop upcall, **3 000 warmup** | 673.91 ns | 536.75 ns | — | +| desktop upcall, **3 000 warmup** | 673.91 ns | 536.75 ns | - | | desktop upcall, **100 000 warmup** | 510–560 ns (11 runs) | 501–530 ns (3) | 506–550 ns (3) | -| wasmJs upcall, **3 000 warmup** | 322.03 ns | 291.67 ns | — | +| wasmJs upcall, **3 000 warmup** | 322.03 ns | 291.67 ns | - | | wasmJs upcall, **100 000 warmup** | 290–304 ns (7 runs) | 287–301 ns (3) | 321–353 ns (3) | The middle column is the lever the wasmJs section above used to prove the defect existed. It used to @@ -284,13 +284,13 @@ is the fix.** **The third column is honest about what is still not fixed, on one target.** Desktop passes it. wasmJs does not, and the reason is not the boundary: filtered to this class alone, wasmJs's *pure-Python* -rows read 23.05 ns and 83.10 ns against the full suite's 15.04 ns and 47.22 ns — 55–74% slower, in +rows read 23.05 ns and 83.10 ns against the full suite's 15.04 ns and 47.22 ns, 55–74% slower, in rows with no boundary in them. CPython is itself a wasm module under V8, so a Node process that lives one second runs the interpreter's own bytecode slower than one that lives ten, and every row is inflated together. Raising the warmup to 300 000 does not move it (solo read 318.83 and 326.21 ns against 100 000's 320.54–337.42), which is what distinguishes a host-lifetime effect from a -call-count effect. **wasmJs's ratio columns do survive filtering** — 2.66–3.13x solo against -2.91–3.14x in the suite — so the ratio is the quantity to quote there, and the absolute column is a +call-count effect. **wasmJs's ratio columns do survive filtering**, 2.66–3.13x solo against +2.91–3.14x in the suite, so the ratio is the quantity to quote there, and the absolute column is a full-suite figure that must be labelled as one. It is left in the table with that label rather than deleted, because desktop's is now sound and dropping both would lose a real result. diff --git a/docs/archive/investigations/android-unregistered-surface.md b/docs/archive/investigations/android-unregistered-surface.md index de644b51f..507716b47 100644 --- a/docs/archive/investigations/android-unregistered-surface.md +++ b/docs/archive/investigations/android-unregistered-surface.md @@ -1,17 +1,17 @@ -> **Superseded** by [docs/investigations/android-unregistered-surface.md](../../investigations/android-unregistered-surface.md) on 2026-10-03 — the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. +> **Superseded** by [docs/investigations/android-unregistered-surface.md](../../investigations/android-unregistered-surface.md) on 2026-10-03, the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. # Reachability Analysis: Android Unregistered Surface > **Status: superseded. Every count and every risk ranking below is historical.** > > The unregistered surface is empty as of the pass recorded in ROADMAP §2: 363 of 367 `external fun` -> are bound through `RegisterNatives`, and the four that are not are deliberate — `ffiAllocUtf8`, +> are bound through `RegisterNatives`, and the four that are not are deliberate, `ffiAllocUtf8`, > `ffiFreeUtf8` and `ffiReadUtf8` are hand-written JNI exports with the correct prologue, and > `echoCriticalNamed` exists in order to be name-linked, so that a benchmark can measure that path. > > The claim here worth carrying forward is that the *ordering* was right and the *counts* were not. > When the last 157 were registered, a recount found **zero** of them reachable from -> `commonMain`/`commonTest` — the earlier passes had already taken the whole reachable surface, so +> `commonMain`/`commonTest`, the earlier passes had already taken the whole reachable surface, so > the "71 reachable" figure below had long since been consumed. The call-graph traces are kept > because they are still the record of which public API path reaches which C function. > @@ -22,7 +22,7 @@ > Recounted from the two files: 187 registered, 178 unregistered (was 145 / 224). Declarations > still carrying a `jstring` across the boundary went from 52 to 6, and none of those six is on > the broken path except `ffiSymbolRaw`, which has no caller anywhere in `src/`. The -> "Critical Risk: Functions taking `String`" section below is therefore resolved in full — not +> "Critical Risk: Functions taking `String`" section below is therefore resolved in full, not > just the four functions it happens to list. What remains is the pointer-only surface, and the > reachability reasoning for that still holds. > diff --git a/docs/archive/investigations/gc-scheduling-investigation.md b/docs/archive/investigations/gc-scheduling-investigation.md index 63565e367..b7b003f54 100644 --- a/docs/archive/investigations/gc-scheduling-investigation.md +++ b/docs/archive/investigations/gc-scheduling-investigation.md @@ -1,16 +1,16 @@ -> **Superseded** by [docs/investigations/gc-scheduling-investigation.md](../../investigations/gc-scheduling-investigation.md) on 2026-10-03 — the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. +> **Superseded** by [docs/investigations/gc-scheduling-investigation.md](../../investigations/gc-scheduling-investigation.md) on 2026-10-03, the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. # Why a pure C API embedder never collects cycles, and never merges a refcount Two things this document is careful about, because it corrects a previous document that was not: - **CPython file:line citations are now verified against the tagged source.** The v3.14.7 tarball - (the version this build pins — `gradle.properties: pythonVersion=3.14.7`) was fetched to + (the version this build pins, `gradle.properties: pythonVersion=3.14.7`) was fetched to `/Volumes/macMini/cpython-src` for §8d and every line number in this document was re-checked against it. Two in §1 were wrong by ~100 lines and are corrected below; the rest were right. Nothing is vendored into this repo. Where a claim matters, §3, §4 and §8 still back it with a measurement taken here rather than with a citation. -- **Every number in §3, §5 and §8 was measured on this machine**, on `desktopTest`, macOS arm64, +- **Every number in §3, §5 and §8 was measured on this machine**: on `desktopTest`, macOS arm64, CPython 3.14.7, both the default and the `-PpythonFreeThreaded=true` builds. - **§8 corrects §7.** A measurement probe in `GCSchedulingMeasurementTest` was leaking the list returned by `gc.get_objects()`, which holds the whole tracked heap, and that leak is what @@ -36,12 +36,12 @@ not. materialises a deferred reference is a collection, not a checkpoint. (§3) - **The checkpoint fixes §1 on the build with the global lock, and on the free-threaded build it often does not.** It fires exactly as designed there and still reclaims nothing. This is *not* - caused by the FFI wrapper's per-call GIL scopes — that hypothesis was tested by holding the + caused by the FFI wrapper's per-call GIL scopes, that hypothesis was tested by holding the checkpoint count fixed and widening the scope, and rejected. (§8) The cause is that free-threaded, `_Py_RunGC` re-asks `gc_should_collect` *after* the checkpoint has read the scheduled bit, and that check gates generation 0 on the growth of the **whole - process's** memory footprint — the JVM's. Python-side allocation cannot move it, so the gate + process's** memory footprint, the JVM's. Python-side allocation cannot move it, so the gate shuts one-way as soon as the JVM's footprint settles. Confirmed by opening and closing the gate on demand, including by faulting in 400 MB of ballast and watching Python cycles get collected with no threshold changed. (§8d) @@ -87,7 +87,7 @@ thread is the object's owner, recorded in `ob_tid`: - **Owner.** `ob_ref_local` is decremented non-atomically. If it reaches zero and the shared count is zero too, `tp_dealloc` runs immediately, on this thread. - **Not the owner.** The decrement goes to `ob_ref_shared` atomically. If that count was already at - zero — i.e. this decrement is the one that takes the object to zero — `tp_dealloc` *cannot* run + zero, i.e. this decrement is the one that takes the object to zero, `tp_dealloc` *cannot* run here, because the object belongs to another thread. It is pushed onto that thread's queue by `_Py_brc_queue_object` (`Objects/object.c:411`, reached from the inline decref in `Include/refcount.h:363`). @@ -97,7 +97,7 @@ The queue is drained by `_Py_brc_merge_refcounts`, whose only caller is, again, **And once a collection has run, a surviving object can become a non-owner case for everybody.** On the way out, `gc_restore_tid` (`Python/gc_free_threading.c:325`) puts each survivor's owning -thread id back — *except* when its shared refcount has been merged, in which case `ob_tid` is left +thread id back, *except* when its shared refcount has been merged, in which case `ob_tid` is left at 0 (`:330`) and no thread matches any more, so every further decrement of that object takes the shared path. (The inherited citation for this was `:205`, which is not the mechanism; merging itself is at `:264`. The behaviour is as described, but it applies to merged survivors rather than @@ -105,14 +105,14 @@ to all of them.) That is the mechanism behind the ROADMAP §9 measurement: 1000 wrappers released by the JVM cleaner, 2003 `Py_DecRef` calls demonstrably made, and `sys.getrefcount` frozen at 1002 through 50 forced -JVM GCs, 500 further C API round trips and two seconds of wall clock — then one trivial bytecode +JVM GCs, 500 further C API round trips and two seconds of wall clock, then one trivial bytecode frame released all thousand at once. ## 3. `CycleCollectionTest`: two defects, and the one that is *not* a checkpoint problem `CycleCollectionTest.testHandleReleasedWhenProxyDiesWithoutCycle` was the last free-threaded -failure. It has two independent causes, and the guard assertion — "the type's count must rise by -exactly `rounds` while the instances are alive, otherwise the probe is not reading a refcount" — +failure. It has two independent causes, and the guard assertion, "the type's count must rise by +exactly `rounds` while the instances are alive, otherwise the probe is not reading a refcount", caught both, exactly as it was written to. ### 3a. The probe read the wrong bytes @@ -135,7 +135,7 @@ struct _object { ``` `Py_REFCNT` there is `ob_ref_local + (ob_ref_shared >> 2)`. Reading eight bytes at offset 0 yields -`ob_tid` — which is why the failure reported a ten-digit number that was *identical* before and +`ob_tid`, which is why the failure reported a ten-digit number that was *identical* before and after creating 100 instances: measured here, `6171668704` both times. The layout above was confirmed against this build by dumping the header (see 3b); it is not taken from documentation. @@ -187,14 +187,14 @@ Reading that off: walks. - **And then `tp_dealloc` gives back exactly 100.** Accounting for the flag bits moving from `_Py_REF_MAYBE_WEAKREF` (1) to `_Py_REF_MERGED` (3) and `ob_ref_local` merging to zero, the total - goes from `1152921504606847077` back to `1152921504606846977` — the pre-instantiation value, + goes from `1152921504606847077` back to `1152921504606846977`, the pre-instantiation value, exactly. So the original ROADMAP claim was right about the *mechanism* (heap types are deferred) and wrong about the *consequence* (that the invariant does not exist and cannot be checked). The invariant holds on both builds. Free-threaded it is merely not **observable** until a collection has materialised the deferred references. The later trace was right that instances are ordinary -reference-counted objects — but the test reads the *type's* count, not an instance's, so that +reference-counted objects, but the test reads the *type's* count, not an instance's, so that correction did not touch the failing assertion. `CycleCollectionTest` therefore takes a `PyGC_Collect()` on either side of its instantiation loop, @@ -203,8 +203,8 @@ exactly where it started" are asserted on both builds, and both now hold. ### What this means for `abi3t` -The conclusion drawn from the wrong premise — "so `abi3t` making `PyObject` an incomplete type -would sink this test" — does not follow either. The test does need to know the header layout, which +The conclusion drawn from the wrong premise, "so `abi3t` making `PyObject` an incomplete type +would sink this test", does not follow either. The test does need to know the header layout, which is a real cost of a direct probe and the one place in this repo that reads inside a `PyObject`. But that is a property of the probe, not of the invariant, and it would be equally solvable by counting through `sys.getrefcount` at the price of the argument's own temporary reference. The library @@ -216,7 +216,7 @@ proper stays clean: `ProxyTypeFactory` writes only into memory it obtained from `Python3.drainPendingReleases()` reaches a checkpoint the cheapest way there is: it calls a cached, already-compiled, zero-argument Python function whose body is `pass`, through `PyObject_CallNoArgs`. That is one `RESUME`, hence one `_CHECK_PERIODIC`, hence one -`_Py_HandlePending` — one BRC merge, one QSBR sweep, and one scheduled collection if one was +`_Py_HandlePending`, one BRC merge, one QSBR sweep, and one scheduled collection if one was scheduled. It is deliberately **not** `PyRun_SimpleString("pass")` or `exec("pass")`: @@ -224,7 +224,7 @@ It is deliberately **not** `PyRun_SimpleString("pass")` or `exec("pass")`: - `PyRun_SimpleString` runs `PyErr_Print()` on failure, which clears the error indicator. This repo has twice had unrelated tests die because a helper wiped an indicator its caller had not read yet; `Python3.exec` is built on `PyRun_String` for exactly that reason. -- `exec("pass")` recompiles a module on every call — 23× the cached call, §5. +- `exec("pass")` recompiles a module on every call, 23× the cached call, §5. `withGIL` takes one automatically at its outermost entry, behind two gates: at most one per `Python3.autoDrainInterval` outermost scopes, and skipped entirely unless `ReleaseCounter.released` @@ -240,7 +240,7 @@ ROADMAP §1 deadlock) in order to drain an empty queue. `PyGC_Collect` is the one Stable ABI function that reclaims on behalf of *another* thread, because `gc_collect_internal` stops the world and walks every thread state, calling `_PyObject_MergePerThreadRefcounts` and `merge_queued_objects` for each. It costs a heap walk -rather than a queue pop — §5 — and, as §3b shows, it is also the only thing that materialises a +rather than a queue pop, §5, and, as §3b shows, it is also the only thing that materialises a deferred reference. ## 5. Measurements @@ -251,11 +251,11 @@ design and would break the moment the checkpoint started compiling or allocating | | ns/op | | |---|---:|---| -| `withGIL { }` — attach and detach, nothing else | 170.51 | the floor | +| `withGIL { }`, attach and detach, nothing else | 170.51 | the floor | | `Python3.drainPendingReleases()` | 302.70 | the checkpoint, ~132 ns over the floor | | `withGIL { Py_MakePendingCalls() }` | 197.34 | cheap, and does not merge | -| `Python3.exec("pass")` | 7 037.04 | 23× the checkpoint — it recompiles | -| `withGIL { PyGC_Collect() }` | 224 625.00 | 742× the checkpoint — heap walk, but drains every thread | +| `Python3.exec("pass")` | 7 037.04 | 23× the checkpoint, it recompiles | +| `withGIL { PyGC_Collect() }` | 224 625.00 | 742× the checkpoint, heap walk, but drains every thread | At the default `autoDrainInterval = 32` the automatic checkpoint amortises to about 4 ns on a ~170 ns outermost scope, and a workload that drops no wrappers skips it on a field compare. @@ -277,7 +277,7 @@ collections §3b requires: ~0.45 ms for the whole test, free-threaded only. actually reaching zero. But note the asymmetry `ProxyTypeFactory` lives with: its hand-written `tp_dealloc` releases the type through the public `Py_DecRef`, which is deferred-unaware, while the matching increment went through the deferred-aware `_Py_INCREF_TYPE` and did not happen. That - balances out once a collection has materialised the references — measured above — but it is + balances out once a collection has materialised the references, measured above, but it is balanced by the collector, not by the two calls. ## 7. GC Accumulation and Reentrancy Measurement @@ -285,7 +285,7 @@ collections §3b requires: ~0.45 ms for the whole test, free-threaded only. Measurements taken to verify the consequences of §1 and the safety of the checkpoint confirm: - **Cyclic Garbage Accumulation (GIL Build)**: A workload creating 10,000 cyclic object groups entirely through the C API (never running a Python bytecode evaluation loop) with `autoDrainInterval = 0` accumulates **20,000+** cyclic garbage objects indefinitely. Because no evaluation loop runs, `_Py_ScheduleGC`'s scheduled bit is never checked by `_CHECK_PERIODIC`, and cyclic GC never occurs. -- **`autoDrainInterval` Effectiveness (GIL build)**: When running the exact same C API workload but with `autoDrainInterval = 32`, `python-multiplatform` evaluates a dummy function (`__pmp_eval_checkpoint__`) to force the evaluation loop to run periodically. This allows `_CHECK_PERIODIC` to see the scheduled bit, trigger `_Py_RunGC()`, and reclaim the cyclic garbage (leaving ~1,970 of ~20,000). **This result is specific to the build with the global lock.** Free-threaded, the same checkpoints frequently reclaim nothing at all — §8. +- **`autoDrainInterval` Effectiveness (GIL build)**: When running the exact same C API workload but with `autoDrainInterval = 32`, `python-multiplatform` evaluates a dummy function (`__pmp_eval_checkpoint__`) to force the evaluation loop to run periodically. This allows `_CHECK_PERIODIC` to see the scheduled bit, trigger `_Py_RunGC()`, and reclaim the cyclic garbage (leaving ~1,970 of ~20,000). **This result is specific to the build with the global lock.** Free-threaded, the same checkpoints frequently reclaim nothing at all, §8. - **`__del__` Reentrancy Risk**: Executing `PyGC_Collect()` or processing a checkpoint can invoke `__del__` methods. A scenario where `__del__` directly calls back into Kotlin (via Panama upcalls) was executed (`GCSchedulingMeasurementTest.testReentrancyDuringCheckpoint`). The reentrancy is safe on the GIL build: it does not deadlock, it does not loop infinitely, and it successfully executes the Kotlin upcall while the garbage collection is in progress. ## 8. Free-threaded: the checkpoint fires and reclaims nothing, and it is not the scope shape @@ -306,7 +306,7 @@ which closes none of its temporaries. `gc.get_objects()` returns a list holding reference to every tracked object in the interpreter**, so a leaked one roots the very garbage the next measurement is trying to see. -That leak is where §7's earlier free-threaded conclusion came from — the claim that "a trailing +That leak is where §7's earlier free-threaded conclusion came from, the claim that "a trailing explicit `gc.collect()` finds nothing further to reclaim, so the residue is not simply garbage waiting for the next checkpoint," and the inference from it that free-threaded CPython was failing to *reclaim* something. With the temporaries released, **an explicit `gc.collect()` reclaims the @@ -322,7 +322,7 @@ text asserted about it. The standing hypothesis was that because free-threading keeps `eval_breaker` per thread state, an FFI wrapper that attaches and detaches around every C API call destroys and recreates the thread -state, discarding the scheduled GC bit with it — and that widening the scope would therefore fix +state, discarding the scheduled GC bit with it, and that widening the scope would therefore fix collection. **Measured and rejected.** Holding the checkpoint count fixed at 2,000 explicit @@ -331,13 +331,13 @@ varying *only* the enclosing scope, free-threaded, residue out of 20,000: | enclosing scope | reps | residue | |---|---:|---| -| none — one `withGIL` per C API call | 5 | 20,000 ×5 | +| none, one `withGIL` per C API call | 5 | 20,000 ×5 | | one per round (~12 C API calls) | 5 | 20,000 ×5 | | one per 100 rounds | 10 | 20,000 ×10 | | one for the whole 10,000-round workload | 10 | 11,562 / 1,280 / 1,280, then 20,000 ×7 | -Widening the scope to 100 rounds — which gives the thread state a long life and hundreds of -allocations before it is destroyed — changes nothing whatsoever. The hypothesis predicts the +Widening the scope to 100 rounds, which gives the thread state a long life and hundreds of +allocations before it is destroyed, changes nothing whatsoever. The hypothesis predicts the opposite, so it is wrong. The single-scope row does not rescue it either: **it is bimodal within one JVM.** Those ten @@ -349,7 +349,7 @@ With the automatic checkpoint instead (`autoDrainInterval = 32`), free-threaded: | enclosing scope | checkpoints taken | residue | |---|---:|---| -| none — one `withGIL` per C API call | 1,875 | 20,000 ×5; also 7,427 and 11,523 in two single-repetition JVMs | +| none, one `withGIL` per C API call | 1,875 | 20,000 ×5; also 7,427 and 11,523 in two single-repetition JVMs | | one per round | 312–313 | 20,000 ×5 | | one per 100 rounds | 3 | 20,000 ×5 | | one for the whole workload | **0** | 20,000 ×3 | @@ -372,7 +372,7 @@ The same sweep on the build with the global lock is stable and near-complete in | one per 100 rounds, `autoDrainInterval = 32` | **3** | 1,600–3,200 | Three checkpoints reclaim ~90% on the GIL build. Two thousand reclaim nothing free-threaded. That -gap — not the scope shape, which is irrelevant on both builds — is the whole finding. +gap, not the scope shape, which is irrelevant on both builds, is the whole finding. ### 8d. Answered: the checkpoint is fine, the collector asks a second question @@ -391,7 +391,7 @@ question, and it is that one that answers no. | | global lock (`Python/gc.c`) | free-threaded (`Python/gc_free_threading.c`) | |---|---|---| | schedules on | `generations[0].count > threshold`, and nothing else (`gc.c:1866`) | `gc_should_collect` (`:2153`) | -| re-checked when the checkpoint runs it | `gc_select_generation` — generation 0 on count alone (`gc.c:1258`) | `gc_should_collect` **again** (`:2328`) | +| re-checked when the checkpoint runs it | `gc_select_generation`, generation 0 on count alone (`gc.c:1258`) | `gc_should_collect` **again** (`:2328`) | | process-memory gate | **none**; `gc.c` has no `last_mem` field and never calls `get_process_mem_usage` | `gc_should_collect_mem_usage` (`:2080`) | | `long_lived_total / 4` | oldest generation only (`gc.c:1300`) | **every** generation-0 decision (`:2131`) | @@ -413,12 +413,12 @@ young.count = 0; deferred_count += young.count; return false; // :2109 ``` `footprint` is `get_process_mem_usage()`, which on macOS is -`task_info(TASK_VM_INFO).phys_footprint` (`:2010`) — **the whole process**, i.e. the JVM. +`task_info(TASK_VM_INFO).phys_footprint` (`:2010`), **the whole process**, i.e. the JVM. `last_mem` is written in exactly one place, after a collection (`:2301`). So, in a JVM-hosted embedder: 20,000 small Python lists cannot move a JVM's physical footprint by the required `last_mem / 10`, the gate returns false, and on the way out it **zeroes `young.count` -into `deferred_count`** — which means the count has to climb from zero to 2,000 again before the +into `deferred_count`**, which means the count has to climb from zero to 2,000 again before the question is even asked again. The only escape is `deferred_count > threshold * 40` = 80,000 container allocations at the default threshold, and this workload makes ~20,000. Checkpoints are irrelevant to all of it; the bit gets read and `gc_collect_main` declines at `:2328`. @@ -435,7 +435,7 @@ The repo has been wrong before about causes derived from source reading, so each turned into a prediction that could fail. All of it is in `python-multiplatform/src/desktopTest/.../ref/FreeThreadedGCGateTest.kt`. -**Which gate declines** — `gc_should_collect_mem_usage`'s failing branch zeroes `young.count` +**Which gate declines**, `gc_should_collect_mem_usage`'s failing branch zeroes `young.count` (`:2109`); the `long_lived_total/4` branch (`:2131`) does not. So the shape of `gc.get_count()[0]` across the workload tells them apart. Free-threaded it is a **sawtooth** (`[1728, 1488, 976, 464, 2464, 1488, 976, 464, 2464, 1488]`) with 0 collections and residue 20,000: @@ -449,36 +449,36 @@ the memory gate, repeatedly. > asked, and produced a perfectly linear trace (`226, 726, 1226, … 19726`) that reads exactly like > proof of the *other* hypothesis. Sampling every 2,000 allocations instead gives the sawtooth. -**The one-line falsifier** — `:2126` says `old[0].threshold == 0` short-circuits the whole chain. -Nothing in `gc.c` reads that field while scheduling. So `gc.set_threshold(2000, 0, 0)` — a setting -about *generation 1* — must switch *generation 0* collection on free-threaded and do nothing at all +**The one-line falsifier**, `:2126` says `old[0].threshold == 0` short-circuits the whole chain. +Nothing in `gc.c` reads that field while scheduling. So `gc.set_threshold(2000, 0, 0)`, a setting +about *generation 1*, must switch *generation 0* collection on free-threaded and do nothing at all with the global lock. Run A/B/A in one JVM so the latch cannot explain the difference; residue out of 20,000, and the number of collections that actually ran: | arm | free-threaded | global lock | |---|---|---| -| `(2000, 10, 10)` | 20,000 — 0 collections | 1,968 — 9 collections | -| `(2000, 0, 0)` | **1,318 — 8 collections** | 1,966 — 5 collections | -| `(2000, 10, 10)` again | 20,000 — 0 collections | 1,970 — 9 collections | +| `(2000, 10, 10)` | 20,000, 0 collections | 1,968, 9 collections | +| `(2000, 0, 0)` | **1,318, 8 collections** | 1,966, 5 collections | +| `(2000, 10, 10)` again | 20,000, 0 collections | 1,970, 9 collections | -**The `deferred_count` arithmetic** — the escape bar is `40 × threshold0`, and `deferred_count` +**The `deferred_count` arithmetic**, the escape bar is `40 × threshold0`, and `deferred_count` grows *only* in the memory gate's failing branch. Against a ~20,000-allocation workload that predicts no collection at threshold0 = 2000 (bar 80,000) or 500 (bar 20,000), and collection at 100 (bar 4,000). Free-threaded: 20,000 / 20,000 / **1,466 with 3 collections**. The same sweep with the global lock just scales smoothly with the threshold (9 / 36 / 172 collections), because there -is no such bar. This also proves the `long_lived_total/4` gate at `:2131` is being *passed* — +is no such bar. This also proves the `long_lived_total/4` gate at `:2131` is being *passed*, nothing else feeds `deferred_count`. -**The latch, driven directly** — if the gate is whole-process footprint, then faulting in 400 MB of +**The latch, driven directly**, if the gate is whole-process footprint, then faulting in 400 MB of ballast partway through the workload must open it, while changing no threshold, allocating no extra Python container and touching nothing the collector tracks. Dropping the ballast must shut it again. Residue out of 20,000: | arm | free-threaded | global lock | |---|---|---| -| baseline | 20,000 — 0 collections | 1,972 — 9 collections | -| 400 MB faulted in midway | **9,754 — 1 collection** | 1,988 — 9 collections | -| ballast dropped | 20,000 — 0 collections | 1,968 — 9 collections | +| baseline | 20,000, 0 collections | 1,972, 9 collections | +| 400 MB faulted in midway | **9,754, 1 collection** | 1,988, 9 collections | +| ballast dropped | 20,000, 0 collections | 1,968, 9 collections | Reproduced identically to the object in three consecutive JVMs. **JVM memory growth, and nothing else, collected Python cycles.** That is the finding: on a free-threaded build the Python cyclic @@ -488,7 +488,7 @@ Python's memory at all. One experiment did **not** confirm what it was built to test and is recorded as such: scaling the workload (10,000 / 30,000 / 60,000 cycles) at default thresholds to cross the 80,000 bar gave 1 / 2 / 2 collections, but the 10,000-cycle arm should have given 0. It ran first in its JVM, while -the footprint was still climbing, so the memory gate — the very thing being controlled for — +the footprint was still climbing, so the memory gate, the very thing being controlled for, supplied the collections. The scaling prediction is untested, not confirmed. #### What this changes @@ -508,8 +508,8 @@ supplied the collections. The scaling prediction is untested, not confirmed. #### The advice does not change -An embedder can open the gate deliberately — `gc.set_threshold(t0, 0, 0)` restores count-driven -collection free-threaded, and a small `threshold0` lowers the `40 × threshold0` bar — but both +An embedder can open the gate deliberately, `gc.set_threshold(t0, 0, 0)` restores count-driven +collection free-threaded, and a small `threshold0` lowers the `40 × threshold0` bar, but both trade throughput for it, and both are tuning of CPython internals that the Stable ABI does not promise. The supported way to collect cycles in an embedder that runs no Python bytecode is still the one that works identically on both builds, documented in §4: `PyGC_Collect()`, at a cost diff --git a/docs/archive/investigations/gil-parking-investigation.md b/docs/archive/investigations/gil-parking-investigation.md index 83709d809..f70a13dbe 100644 --- a/docs/archive/investigations/gil-parking-investigation.md +++ b/docs/archive/investigations/gil-parking-investigation.md @@ -1,4 +1,4 @@ -> **Superseded** by [docs/investigations/gil-parking-investigation.md](../../investigations/gil-parking-investigation.md) on 2026-10-03 — the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. +> **Superseded** by [docs/investigations/gil-parking-investigation.md](../../investigations/gil-parking-investigation.md) on 2026-10-03, the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. # Why enabling `PyEval_SaveThread()` in `Python3.initialize()` crashes desktopTest @@ -10,7 +10,7 @@ below are cited from `python/cpython` at tag `v3.14.0` on GitHub, fetched read-o **Ruled out, with source evidence:** the failure is not "`PyGILState_Ensure` creates a second, competing `PyThreadState`" on the initialising thread. It reuses the *same* `PyThreadState*` that `Py_Initialize()` created. This is CPython's documented, intended mechanism for exactly this -pattern (park, then re-enter from a C thread later) — see §2 and §3. +pattern (park, then re-enter from a C thread later), see §2 and §3. **Established, with source evidence, but not sufficient by itself:** `PyEval_SaveThread()` leaves the calling OS thread's slot in the GILState TSS (`autoTSSkey`) pointing at the parked @@ -33,7 +33,7 @@ whichever thread first touches it. `desktopTest`'s Gradle configuration (`python-multiplatform/build.gradle.kts:724`) sets no `maxParallelForks`, no `junit.jupiter.execution.parallel.*` property exists anywhere in the repo, and no `junit-platform.properties` exists. Gradle's `Test` task therefore runs the whole suite -sequentially inside one worker JVM, on the JVM's single test-execution thread — which is named +sequentially inside one worker JVM, on the JVM's single test-execution thread, which is named `"Test worker"` by Gradle, matching the thread name in today's crash report verbatim. So `Python3.initialize()` and `EmbedApiLowLevelTest.importReturnsANewReferenceAndMissingModulesReportFailure` run on the **same OS thread**, in the same process, with nothing else attaching in between. @@ -97,7 +97,7 @@ GIL is released, and the per-OS-thread "current thread state" pointer own comment above states outright that detaching deliberately leaves it alone. By the end of `Py_Initialize()` (before the parking call), the calling thread's GILState TSS slot -is already bound to the main thread's `PyThreadState`, with `gilstate_counter == 1` — confirmed by +is already bound to the main thread's `PyThreadState`, with `gilstate_counter == 1`, confirmed by `_PyGILState_SetTstate()`'s debug assertions (`Python/pystate.c`): ```c @@ -106,8 +106,8 @@ assert(gilstate_tss_get(runtime) == tstate); assert(tstate->gilstate_counter == 1); ``` -(`bind_gilstate_tstate` is what performs that binding, called from `tstate_activate` — i.e. from -inside `_PyThreadState_Attach`/`PyEval_RestoreThread` — the first time the main thread is +(`bind_gilstate_tstate` is what performs that binding, called from `tstate_activate`, i.e. from +inside `_PyThreadState_Attach`/`PyEval_RestoreThread`, the first time the main thread is attached during startup.) ## 3. What `PyGILState_Ensure()` then does on that same thread @@ -146,16 +146,16 @@ the "new thread" branch (which would create a genuinely second `PyThreadState`) `tcur` is the exact same `PyThreadState*` `Py_Initialize()` created. `holds_gil(tcur)` compares `tcur` against `current_fast_get()`, which is `NULL` after parking, so `has_gil = 0`, and `PyEval_RestoreThread(tcur)` re-attaches that same object (`current_fast_set`, GIL re-acquired, -`tstate_activate` — which is a no-op on the already-bound gilstate slot). `gilstate_counter` goes +`tstate_activate`, which is a no-op on the already-bound gilstate slot). `gilstate_counter` goes 1 → 2. `PyGILState_Release()` mirrors this exactly: decrements 2 → 1, and because the saved `oldstate` was `PyGILState_UNLOCKED`, it re-parks with another internal `PyEval_SaveThread()` call. This is self-consistent and, cycled repeatedly, returns to the same `gilstate_counter == 1`, detached baseline every time. **Nothing in this path frees, reallocates, or otherwise clears the -frame stack, exception state, or recursion bookkeeping that the object already carried** — it is +frame stack, exception state, or recursion bookkeeping that the object already carried**, it is literally the same object, reused. This rules out "two thread states corrupting each other's interpreter frame stack" as the -mechanism, at least on the initialising thread — there is only ever one `PyThreadState` in play +mechanism, at least on the initialising thread, there is only ever one `PyThreadState` in play here. It does not, by itself, identify what *does* go wrong. ## 4. Whether `PyGILState_Release` is handed an invalid state @@ -166,7 +166,7 @@ still-registered `tcur`. There is no thread-identity mismatch and no stale/delet `Release` in this flow. (Contrast with the *different*, already-fixed bug documented in `GCLeakTest.desktop.kt:6-11`: an earlier version called `PyEval_SaveThread()` from `forceGC()` on a thread that was not attached at all. `PyEval_SaveThread`'s body dereferences -`_PyThreadState_GET()` unconditionally — on a PGO/LTO release build, `assert()`s such as +`_PyThreadState_GET()` unconditionally, on a PGO/LTO release build, `assert()`s such as `detach_thread`'s `assert(tstate == current_fast_get())` compile out, so a `NULL` tstate reaches `detach_thread` and is dereferenced (`tstate->critical_section`) with no check. That is a plain null-pointer crash from calling `PyEval_SaveThread` while unattached, and it is a different bug @@ -186,7 +186,7 @@ knowing before trusting older blog posts or Stack Overflow answers about this AP Two candidates for what actually breaks, neither confirmed: -**Candidate A — stale per-thread C-stack recursion limits.** Each `_PyThreadStateImpl` carries +**Candidate A, stale per-thread C-stack recursion limits.** Each `_PyThreadStateImpl` carries `c_stack_top` / `c_stack_soft_limit` / `c_stack_hard_limit`, computed **once** by `_Py_InitializeRecursionLimits()` from the current machine stack pointer, and only if `c_stack_hard_limit == 0`: @@ -204,13 +204,13 @@ _PyThreadState_Attach(PyThreadState *tstate) ``` Because our thread reuses the same `PyThreadState` object across every park/re-enter cycle, this -calibration happens exactly once — at whatever C call depth `Py_Initialize()`'s *first* internal -attach happened at — and is **never** recomputed on later re-entries from a structurally different +calibration happens exactly once, at whatever C call depth `Py_Initialize()`'s *first* internal +attach happened at, and is **never** recomputed on later re-entries from a structurally different call depth (the JVM/Panama boundary is a different C stack shape than CPython's own `Py_InitializeFromConfig` call chain). `_Py_CheckRecursiveCall` compares the live stack pointer against this stale `c_stack_hard_limit` on every recursive C call in the eval loop (`if (here_addr < c_stack_hard_limit) Py_FatalError(...)`). This is a real, citable staleness bug -in the reuse path — but by itself it explains a *miscalibrated guard*, and the failure it produces +in the reuse path, but by itself it explains a *miscalibrated guard*, and the failure it produces when it fires is a `RecursionError` or a `Py_FatalError` with the text "Unrecoverable stack overflow", neither of which matches today's raw `SIGSEGV` (no such message was reported), and a single-frame `PyImport_ImportModule("sys")` call is not deep enough to plausibly exhaust several @@ -218,16 +218,16 @@ hundred KB to MB of C stack on its own. **I could not make this candidate fit th without more evidence, and flag it mainly because it's a real, separate latent hazard in the reuse pattern that the fix should be aware of regardless of whether it explains today's crash.** -**Candidate B — something the eval loop touches on the initial (never-parked) startup path that +**Candidate B, something the eval loop touches on the initial (never-parked) startup path that `import sys`'s cache-hit-with-Python-level-bookkeeping path exercises differently.** `PyImport_ImportModule("sys")` for an already-loaded module does not simply `PyDict_GetItemString` -and return — `import_ensure_initialized()` (in `Modules/import.c`, not fetched in this +and return, `import_ensure_initialized()` (in `Modules/import.c`, not fetched in this investigation) can invoke Python-level machinery (attribute lookups on `__spec__`, method calls), which is consistent with the reported crash frames (`PyObject_CallMethodObjArgs` → `_PyFunction_Vectorcall` → `DICT_MERGE`) being genuine bytecode execution rather than a pure C -lookup. I could not determine, from source reading alone, why *this* bytecode execution — on a +lookup. I could not determine, from source reading alone, why *this* bytecode execution, on a thread whose `PyThreadState` is otherwise identical to the one `Py_Initialize()` used successfully -— would be unsafe. This is the open question the experiment in §6 is aimed at. +would be unsafe. This is the open question the experiment in §6 is aimed at. I was not able to reduce this to one explanation. Ranked: reuse of the same `PyThreadState` across the park/re-enter boundary is established fact and is the right place to keep looking; which piece @@ -236,7 +236,7 @@ failure shape and Candidate B unconfirmed. ## 6. Minimal experiment (does not require the full suite) -Add a single desktop test — not to run today, but to hand to the next attempt — that escalates in +Add a single desktop test, not to run today, but to hand to the next attempt, that escalates in four steps on **one thread**, printing/asserting between each so a failure localizes to a specific step instead of reporting "0 tests run": @@ -295,7 +295,7 @@ fun parkThenReenterSameThreadIsolatesTheFailure() { } ``` -This is deliberately not `@Test`-annotated as ready to run blind — the next attempt should run it +This is deliberately not `@Test`-annotated as ready to run blind, the next attempt should run it alone (`--tests` filter), on a quiet tree, and stop at the first step that crashes rather than proceeding to the next. That directly gives a call-depth-bounded bisection of the failure, which the "audit every call site" and "run the whole suite" attempts so far have not produced. @@ -306,8 +306,8 @@ investigation (see §7). ## 7. What the fix would look like, under each candidate -- **If Candidate A (stale C-stack calibration) turns out to matter**, no amount of call-site - guarding fixes it — the fix has to either recalibrate `c_stack_hard_limit` on every reattachment +- **If Candidate A (stale C-stack calibration) turns out to matter**: no amount of call-site + guarding fixes it, the fix has to either recalibrate `c_stack_hard_limit` on every reattachment (not something this project's API surface exposes; would need a non-limited-API call or a CPython-side change) or ensure the *first* attach happens at a call depth structurally representative of where Python will actually run later. Concretely: call `Py_Initialize()` @@ -317,14 +317,14 @@ investigation (see §7). one-line fix. - **If Candidate B (something Python-level import bookkeeping depends on) turns out to matter**, - the fix likely isn't about *this* call site specifically — `PyImport_ImportModule("sys")` is - representative of "any first bytecode execution after reattachment", not a special case — so + the fix likely isn't about *this* call site specifically, `PyImport_ImportModule("sys")` is + representative of "any first bytecode execution after reattachment", not a special case, so narrowing further requires the bisection in §6 before a fix can be scoped at all. - **If the experiment shows Step 1 or Step 2 already crashes** (i.e. even bare reattachment or a pure-C call is unsafe), that overturns the "reuse is fine, something else breaks" framing of this whole document, and the next place to look is the desktop Panama bindings for exactly these four - functions (`bindings.kt:41-50`) — this project has a documented, repeated history of exactly this + functions (`bindings.kt:41-50`), this project has a documented, repeated history of exactly this class of bug (`invoke` vs `invokeExact`, `ADDRESS` vs `JAVA_LONG`), and while a quick read during this investigation found `PyEval_SaveThread`/`RestoreThread`/`PyGILState_Ensure`/`Release` all using `invokeExact` and the project's `P = JAVA_LONG` pointer convention correctly, that read was @@ -336,23 +336,23 @@ investigation (see §7). fundamentally what's unsafe in this embedding (as opposed to something narrower), the design question becomes whether the initialising thread should ever call `PyGILState_Ensure` again at all. An alternative shape: run `Py_Initialize()` on a dedicated thread that parks once and is - never reused for Python work afterward — every subsequent call, including from what is currently + never reused for Python work afterward, every subsequent call, including from what is currently "the same thread" in tests, would then go through the "first time on this OS thread" branch of `PyGILState_Ensure`, which creates a genuinely fresh `PyThreadState` rather than resurrecting the init-time one. That is a bigger change than uncommenting the parking line (it changes where `Py_Initialize()`/`Py_Finalize()` are allowed to be called from, and `Python3.finalize()`'s `PyEval_RestoreThread(mainThreadState)` call would need to run on that same dedicated thread too), and it should not be attempted until the experiment in §6 has actually localized the fault - — building it on a guess would repeat the pattern this document was asked to stop. + , building it on a guess would repeat the pattern this document was asked to stop. ## 8. What could not be determined - **The exact opcode-level or memory-level reason `DICT_MERGE` faults.** Both candidates in §5 are plausible mechanisms consistent with *some* of the evidence, but neither was confirmed against the specific crash. This needs the experiment in §6, or a debugger session (`lldb` on the core, - or a build with `Py_DEBUG` to turn the compiled-out `assert()`s back on) — deliberately not + or a build with `Py_DEBUG` to turn the compiled-out `assert()`s back on), deliberately not attempted here per the "prefer reading over running" instruction for this task. -- **Whether `import_ensure_initialized`'s Python-level path (Candidate B) is real** — I did not +- **Whether `import_ensure_initialized`'s Python-level path (Candidate B) is real**: I did not fetch `Modules/import.c`, so the claim that a `sys.modules` cache hit still runs Python-level code is inferred from general CPython import-machinery knowledge, not confirmed against 3.14 source in this session. diff --git a/docs/archive/investigations/jni-call-convention-audit.md b/docs/archive/investigations/jni-call-convention-audit.md index 874b23750..9163dbf47 100644 --- a/docs/archive/investigations/jni-call-convention-audit.md +++ b/docs/archive/investigations/jni-call-convention-audit.md @@ -1,4 +1,4 @@ -> **Superseded** by [docs/investigations/jni-call-convention-audit.md](../../investigations/jni-call-convention-audit.md) on 2026-10-03 — the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. +> **Superseded** by [docs/investigations/jni-call-convention-audit.md](../../investigations/jni-call-convention-audit.md) on 2026-10-03, the conclusion, numbers and reproduction steps live there; this is the full original narrative, kept for history. # JNI Calling Convention Audit @@ -200,12 +200,12 @@ Based on CPython's documented semantics, the following rules apply when classify Written after checking section 2 against this repository's own measurements and against the registration table as it stands at 187 entries (the audit read 71). -**The table holds 366 today**, so this resolution is itself a snapshot — roughly 180 registrations +**The table holds 366 today**, so this resolution is itself a snapshot, roughly 180 registrations have never been classified by any hand-written pass. That is why the classification moved into `JniCallConventionClassificationTest`, which re-derives it from `bindings.kt`, `jni_onload.def` and the bundled CPython headers on every desktop build. Its re-derivation agrees with the judgement -recorded here — the eight GC-blocking call sites reproduce exactly, no function was wrongly -promoted, and all 366 prologues match their convention — so what was missing was never correctness, +recorded here, the eight GC-blocking call sites reproduce exactly, no function was wrongly +promoted, and all 366 prologues match their convention, so what was missing was never correctness, only a way for it to stay correct. ## The promotion to `@CriticalNative` is rejected @@ -226,7 +226,7 @@ API 26-31, only ~2-4ns on API 34+". That is backwards. Measured here, net of the `@CriticalNative` is a **loss** on the two newest levels measured. Taking section 2 as written would slow every one of those eight functions down on any device from Android 14 onward. -It could be had behind the existing `preferFastNative` branch — critical below 34, ordinary above. +It could be had behind the existing `preferFastNative` branch, critical below 34, ordinary above. That is not being done, for three reasons. **It is not a relabel.** `@CriticalNative` receives no `JNIEnv` or `jclass`, so it cannot reuse the @@ -240,11 +240,11 @@ registration name does not change. | | audit says | actually | |---|---|---| | `PyGILState_Release` | "unlocks mutex (non-blocking)" | the release that drops the counter to zero deletes the thread state; clearing a thread state decrefs its dict and exception state, so `__del__` runs | -| `PyThreadState_GetDict` | "reads struct pointer" | allocates the thread dict with `PyDict_New` on first call per thread — a GC-tracked allocation, so rule 2 applies | +| `PyThreadState_GetDict` | "reads struct pointer" | allocates the thread dict with `PyDict_New` on first call per thread, a GC-tracked allocation, so rule 2 applies | | `PyEval_InitThreads` | "initializes locking" | a no-op retained for the stable ABI (removed from the C API in 3.13; still exported by the bundled `libpython`, confirmed with `nm -D`). Promoting a no-op buys the convention delta and nothing else | That leaves `Py_IncRef`, `Py_NewRef`, `Py_XNewRef`, `PyGILState_GetThisThreadState` and -`PyEval_SaveThread` as genuine leaves — and of those, only the three refcount operations are on a +`PyEval_SaveThread` as genuine leaves, and of those, only the three refcount operations are on a path called often enough for tens of nanoseconds to accumulate. **The audit's own confidence claim does not hold.** "Unresolved entries: none" is not available @@ -256,12 +256,12 @@ The static sweep found one place where the current configuration contradicts the it is a demotion rather than a promotion. `PyList_GetItemRaw` was registered `@CriticalNative` with **no `@FastNative` twin and no -`preferFastNative` branch** — the only declaration in `bindings.kt` that ignores the device axis. +`preferFastNative` branch**, the only declaration in `bindings.kt` that ignores the device axis. It is also the per-element call of bulk list iteration, so the penalty multiplies by N. ROADMAP §5 measured `list → LongArray`, 1000 elements, at 50065.89 ns on API 36 hardware: 50 ns per element, against a `@CriticalNative` net cost of 44.05 ns on that device. That 50 ns also covers the loop and the `toNativePointer` conversion, so 44.05 is an upper bound on the convention's share rather than -an attribution — but it is an upper bound of 88%, which is enough to act on. +an attribution, but it is an upper bound of 88%, which is enough to act on. So a `@FastNative` twin was added (`f_PyList_GetItemRaw` in `jni_onload.def`, `PyList_GetItemRawF` in `bindings.kt`) and the call site now branches on `preferFastNative`, the @@ -279,7 +279,7 @@ composition was paying for a configuration mistake rather than for crossings. ## The registrations added since the audit: 116, all ordinary, no defects -The audit read 71 entries. The table has 187 — 74 added at `9abc8edd`, 42 at `aea09585`, none +The audit read 71 entries. The table has 187, 74 added at `9abc8edd`, 42 at `aea09585`, none removed. **Every one of the 116 is registered `N`-suffixed and declared plain `@JvmStatic external fun`.** @@ -323,16 +323,16 @@ path and all eight can allocate a GC-tracked exception on the failure path: | call site | success path | failure path | |---|---|---| -| `Py_IsInitialized` | reads a global | — | -| `Py_GetVersion` | returns a static `const char*` | — | -| `PyErr_Occurred` | reads the thread state, borrowed | — | +| `Py_IsInitialized` | reads a global | - | +| `Py_GetVersion` | returns a static `const char*` | - | +| `PyErr_Occurred` | reads the thread state, borrowed | - | | `PyLong_FromLongLong` | non-GC allocation | preallocated `MemoryError` | | `PyUnicode_FromString` | non-GC allocation | `UnicodeDecodeError` on malformed UTF-8 | | `PyUnicode_AsUTF8` | caches the UTF-8 form | `UnicodeEncodeError` on lone surrogates | | `PyList_Size` | reads `ob_size` | `PyErr_BadInternalCall` on a non-list | | `PyList_GetItem` | indexes the item array | `IndexError` out of range | -That second column is second-order and conditional — the exception instance is GC-tracked, so it +That second column is second-order and conditional, the exception instance is GC-tracked, so it can cross the collection threshold, so the cyclic collector can run a `__del__`, which with upcalls live can be Kotlin. It is not a live defect and it is not a reason to demote these eight, which are the measured hot path. It is the reason step 4 of the decision procedure does not pretend "provably diff --git a/docs/archive/kotlin-extensions-in-python-pyi.md b/docs/archive/kotlin-extensions-in-python-pyi.md index dbf408b54..cec23e0ac 100644 --- a/docs/archive/kotlin-extensions-in-python-pyi.md +++ b/docs/archive/kotlin-extensions-in-python-pyi.md @@ -1,10 +1,10 @@ -# Kotlin extensions in Python — the original `.pyi` section (archived) +# Kotlin extensions in Python, the original `.pyi` section (archived) > **Superseded** by [`docs/design/pyi-generation-design.md`](../design/pyi-generation-design.md) and > [SPEC.md](../SPEC.md) B-7 on 2026-10-03; kept for history. > > This is §4.5 of `docs/design/kotlin-extensions-in-python.md` as it stood before 2026-10-03. It -> proposed that this repository's Gradle plugin emit a Pythonic stub — extension-as-method, the +> proposed that this repository's Gradle plugin emit a Pythonic stub, extension-as-method, the > `Modifier` metaclass, `@overload` sets, `Dp | float` for allow-listed value classes. The plugin now > emits Kotlin-named stubs with boundary-type annotations only, and the Pythonic stub product belongs > to pythonx-compose. The three measured bullets (`@overload` is mandatory for 33 of 130 `Modifier` @@ -23,7 +23,7 @@ method, and the metaclass of §4.2 is expressible: > instance method and rejects every class-object call; see §4.2's note and > `docs/archive/pyi-generation-pythonic-stubs.md` §4.3. It is kept here because the three bullets that follow it are > about `@overload`, `Dp | float` and defaults, and all three survive unchanged into the shape that -> does work — `padding: ClassVar[_Modifier_padding]` where `_Modifier_padding` is a Protocol whose +> does work, `padding: ClassVar[_Modifier_padding]` where `_Modifier_padding` is a Protocol whose > `__call__` carries exactly these overloads (`docs/archive/pyi-generation-pythonic-stubs.md` §4.4). What changes is > where the overloads are written, not which overloads there are. @@ -47,7 +47,7 @@ Three things follow from the measurements: - **`@overload` is mandatory, not optional.** 33 of 130 `Modifier` names carry more than one overload, and they are the load-bearing ones (§3.1). A generator that emits one signature per name will mistype `padding`, `size`, `background`, `border` and `clickable`. -- **`Dp | float` is how the allowlist shows up in the stub**, and a type on the reject list is +- **`Dp | float` is how the allowlist shows up in the stub**: and a type on the reject list is stubbed as the proxy alone. The stub is then the documentation for §4.4's asymmetry. - **Defaults must be carried.** 58% of `Modifier` extension parameters declare one; a stub with everything required would be wrong about most of the API. Metadata's `declaresDefaultValue` is diff --git a/docs/archive/pyi-generation-pythonic-stubs.md b/docs/archive/pyi-generation-pythonic-stubs.md index 96fb668a2..58b2534b5 100644 --- a/docs/archive/pyi-generation-pythonic-stubs.md +++ b/docs/archive/pyi-generation-pythonic-stubs.md @@ -1,4 +1,4 @@ -# Generating `.pyi` — the Pythonic stub product (archived) +# Generating `.pyi`, the Pythonic stub product (archived) > **Superseded** by [`docs/design/pyi-generation-design.md`](../design/pyi-generation-design.md) and > [SPEC.md](../SPEC.md) B-7 on 2026-10-03; the Pythonic stub product belongs to pythonx-compose; kept @@ -26,7 +26,7 @@ `pythonx` adapts Kotlin generically rather than wrapping function by function (`docs/design/ecosystem.md` §5b): a module `__getattr__` finds the corresponding binding, adapts it once and caches it. Nothing -an editor can see is ever enumerated. `.pyi` is what pays that back — the stubs carry the fully +an editor can see is ever enumerated. `.pyi` is what pays that back, the stubs carry the fully enumerated Pythonic surface the adapter will produce, so an IDE sees everything while the runtime enumerates nothing. Generation belongs to the Gradle plugin, the way PyREPL did it; that is settled and this file does not revisit it. @@ -57,7 +57,7 @@ descriptor, `list` = the hand-maintained allowlist of §3.4. | `ByteArray` | `bytes` | Km/KS | | | `Unit` | `None` | Km/KS | return position only | | `Nothing` | `typing.Never` | Km/KS | a function returning it never returns; `Never` is the exact Python spelling | -| `T?` | `T \| None` | `KmType.isNullable`, `KSType.isMarkedNullable` | **not bindable today** — `resolveKotlinType` declines nullable outright. §3.3 | +| `T?` | `T \| None` | `KmType.isNullable`, `KSType.isMarkedNullable` | **not bindable today**, `resolveKotlinType` declines nullable outright. §3.3 | | `List` `MutableList` | `list[T]` | Km/KS type args | not bindable today (the type gate) | | `Set` | `set[T]` | | | | `Map` | `dict[K, V]` | | | @@ -66,17 +66,17 @@ descriptor, `list` = the hand-maintained allowlist of §3.4. | `IntArray` `LongArray` `…` | `list[int]` etc. | desc/Km | `ByteArray` is the exception above | | `() -> Unit` | `Callable[[], None]` | Km classifier `kotlin/Function0` | §3.5 | | `(A) -> B` | `Callable[[A], B]` | `kotlin/FunctionN` type args | | -| `suspend (…) -> T` | — | `KmFunction.isSuspend`, `KSType.isSuspendFunctionType` | declined by both producers; must not be stubbed | +| `suspend (…) -> T` | - | `KmFunction.isSuspend`, `KSType.isSuspendFunctionType` | declined by both producers; must not be stubbed | | value class on the allowlist | `Dp \| float` | Km + `list` | §3.4 | | value class off the allowlist | `TextUnit` | Km | §3.4 | -| any other class/interface | its own stub name | Km/KS | requires that class to have a stub in the same namespace, which for a `TypeTag.OBJECT` handle is a promise the runtime does not yet keep — §7 | -| a generic type *parameter* (`fun f(x: T)`) | — | | `BindingPolicy` rejects these before the binder sees them; stubbing what cannot be called would be a lie. §7 | +| any other class/interface | its own stub name | Km/KS | requires that class to have a stub in the same namespace, which for a `TypeTag.OBJECT` handle is a promise the runtime does not yet keep, §7 | +| a generic type *parameter* (`fun f(x: T)`) | - | | `BindingPolicy` rejects these before the binder sees them; stubbing what cannot be called would be a lie. §7 | > Archive note: this table maps the **declared Kotlin type**; it was implemented as `PythonTypes.kt` > for the Pythonic product and removed with it. The current stubs annotate the **boundary** type -> instead — see `docs/design/pyi-generation-design.md` §3.1. +> instead, see `docs/design/pyi-generation-design.md` §3.1. -### 3.4 Value classes — how the allowlist shows up +### 3.4 Value classes, how the allowlist shows up `docs/design/kotlin-extensions-in-python.md` §4.4 decided this and measured why a machine rule fails: the rule "coerce iff the value class has a public constructor taking exactly its underlying type" @@ -89,7 +89,7 @@ In a stub the distinction is one union member: | Kotlin parameter | `.pyi` | why | |---|---|---| | `Dp` (on the allowlist) | `Dp \| float` | public constructor, wraps a plain `Float`, and it *is* the identity on that float. 62% of value-class parameter occurrences on `Modifier` | -| `TextUnit` (rejected) | `TextUnit` | packed `Long`; raw `16` decodes as `Unspecified` — silently no value at all | +| `TextUnit` (rejected) | `TextUnit` | packed `Long`; raw `16` decodes as `Unspecified`, silently no value at all | | `Color` (rejected despite passing the constructor test) | `Color` | `Color(int)` is `ULong(value) shl 32`; raw `0xFFFF0000` is transparent black through the constructor and red through the factory | | every `packedValue` class (`Offset`, `Size`, `DpSize`, `IntSize`, `IntOffset`, `DpOffset`, `TransformOrigin`, `CornerRadius`, `Constraints`) | the proxy alone | a user-meaningful number is not the stored number, and none of these failures raises | @@ -99,7 +99,7 @@ In a stub the distinction is one union member: error: Argument "font_size" to "Text" has incompatible type "int"; expected "TextUnit" while `Modifier.padding(16)` in the same file checked clean. So the stub is not merely documentation -of the asymmetry — a checker acts on it, and the user finds out at edit time rather than seeing a +of the asymmetry, a checker acts on it, and the user finds out at edit time rather than seeing a label render with no font size. What a stub cannot carry is §4.4's *third* clause, the error message that names the constructor to @@ -124,7 +124,7 @@ Two things the stub should do and one it should not: `Column(modifier=Modifier.size(4), content=lambda: Text("x"))` resolves clean. - **`@Composable` on a lambda parameter is not expressible and should not be faked.** A `@Composable () -> Unit` and a `() -> Unit` are the same Python `Callable[[], None]`. Kotlin's - restriction — a composable lambda may only be invoked in a composable context — has no Python + restriction, a composable lambda may only be invoked in a composable context, has no Python counterpart, and inventing a distinct alias would give a checker a rule it cannot enforce. `docs/design/ecosystem.md` §5b already decided the composer is threaded as an ordinary value. - **Do not stub the synthetic parameters.** `$composer` and `$changed` are in the JVM descriptor and @@ -159,12 +159,12 @@ listed. mypy issued **no** overlap diagnostic. That last fact is the one to be careful about. mypy reports overlapping overloads only when their *return types* are incompatible, and every `Modifier` extension returns `Modifier`. So variants 2 and -3 genuinely overlap — `p(8)` matches `all` and also matches `horizontal` positionally — and the +3 genuinely overlap, `p(8)` matches `all` and also matches `horizontal` positionally, and the checker silently takes the **first** match. **Judged: overload order is part of the generated output and must be deterministic and arity-ascending.** In the run above, arity-ascending put `all: Dp` before `horizontal, vertical`, and -`p(8)` selected `all` — the right answer, but only because of the order. Two further consequences: +`p(8)` selected `all`, the right answer, but only because of the order. Two further consequences: - The stub's order must be **the same order the Python-side dispatcher resolves in**. If the runtime dispatcher (`docs/design/kotlin-extensions-in-python.md` §6, unsolved) picks differently, the stub lies @@ -177,7 +177,7 @@ Kotlin parameter names go through the same snake_case conversion as everything e (`paddingValues` → `padding_values`), which `pythonx-compose` already does by hand: `text.py` maps `font_size` → `fontSize`, `letter_spacing` → `letterSpacing`, and 14 more. -## 4. Extension functions — and the metaclass proposal does not survive contact +## 4. Extension functions, and the metaclass proposal does not survive contact This is the section the Compose surface turns on: 604 public top-level extension functions over 136 receivers, and 177 chainable ones on `Modifier` alone. @@ -256,7 +256,7 @@ Modifier3().padding(16) # -> correct ``` But declaring the metaclass side as `@property -> _PaddingCall` in the stub changes nothing for -mypy — it still resolves the class's own method and produces the same twelve errors. **mypy does not +mypy, it still resolves the class's own method and produces the same twelve errors. **mypy does not model metaclass data-descriptor precedence.** ### 4.4 Judged: callback-Protocol attributes, and the stub need not mirror the runtime mechanism @@ -277,7 +277,7 @@ class Modifier: background: ClassVar[_Modifier_background] ``` -**Measured** — the same twenty-line user file that produced ten errors against §4.2 produces exactly +**Measured**, the same twenty-line user file that produced ten errors against §4.2 produces exactly the two intended ones: | expression | revealed / result | @@ -290,14 +290,14 @@ the two intended ones: | `Modifier.size(4).padding(2)` | `Modifier` | | `m.padding(16)` where `m: Modifier` | `Modifier` | | `takes_modifier(Modifier.padding(16))` where `def takes_modifier(m: Modifier)` | clean | -| `Text("hi", font_size=16)` | **error** — `int` is not `TextUnit` (intended, §3.4) | +| `Text("hi", font_size=16)` | **error**, `int` is not `TextUnit` (intended, §3.4) | | `Modifier.padding("nope")` | **error**, with all overload variants listed (intended) | -`ClassVar` is not load-bearing for inference — a plain `padding: _Modifier_padding` gives byte-identical -results — but it is the honest annotation, and it stops a checker accepting `m.padding = something`. +`ClassVar` is not load-bearing for inference, a plain `padding: _Modifier_padding` gives byte-identical +results, but it is the honest annotation, and it stops a checker accepting `m.padding = something`. The point that makes this legitimate rather than a trick: **a `.pyi` completely replaces the `.py` -for a checker, so the stub's mechanism and the runtime's mechanism do not have to agree — only the +for a checker, so the stub's mechanism and the runtime's mechanism do not have to agree, only the types do.** The runtime uses the hybrid descriptor of §4.3, which binds `self` correctly for both spellings; the stub says "a callable attribute", which is what a checker can act on. The one inaccuracy is that a checker believes `Modifier.padding` and `m.padding` are the same object, and @@ -308,7 +308,7 @@ nothing observable depends on that. | alternative | why not | |---|---| | the metaclass of §4.2 as written | measured not to work, in both mypy and CPython (§4.3) | -| metaclass whose members are `@property -> Protocol` | works at run time, does not work in mypy — the class's own member still wins. Would give a *correct runtime* with a *wrong IDE*, which is the worst of the two | +| metaclass whose members are `@property -> Protocol` | works at run time, does not work in mypy, the class's own member still wins. Would give a *correct runtime* with a *wrong IDE*, which is the worst of the two | | bind the module-level name `Modifier` to the companion instance | already rejected in §4.2 of the other document and still right: it destroys `Modifier` as an annotation, and the stubs exist so that annotation works | | drop the `Modifier.padding(...)` spelling, require `Modifier().padding(...)` | changes the API away from Kotlin's for a reason that is an implementation detail of Python attribute lookup | | emit both the metaclass **and** the Protocol attributes | the class attribute shadows the metaclass in both engines, so the metaclass is dead text that a reader will believe | @@ -316,12 +316,12 @@ nothing observable depends on that. **Cost, stated:** one Protocol class per `(receiver, name)` pair. For `Modifier` that is 130 extra class definitions in `pythonx/compose/ui/__init__.pyi`; across the 507 distinct `(receiver, name)` pairs in corpus A, ~507 if every receiver is stubbed. They are generated, never read by a human, and -`.pyi` files are not executed — but a 500-class stub file is a real indexing cost for an IDE and it +`.pyi` files are not executed, but a 500-class stub file is a real indexing cost for an IDE and it has not been measured. **Not verified: PyCharm.** Callback protocols are standard typing and PyCharm supports `Protocol`, but whether its inference resolves `Modifier.padding(16)` through a `ClassVar[Protocol]` was not -tested. **This is the single most load-bearing unverified claim in this document** — §4.4 is chosen +tested. **This is the single most load-bearing unverified claim in this document**, §4.4 is chosen over §4.2 on mypy evidence alone, and if PyCharm follows the runtime MRO rule the way mypy does then §4.4 works there too, while if it has its own metaclass handling the conclusion could differ. Verify before implementing. @@ -335,7 +335,7 @@ Verify before implementing. ### 5.1 Observed: the shape of `pythonx-compose` `/Volumes/macMini/thisisthepy/pythonx-compose/pythonx/` contains **only** `compose/`, and there is -**no `pythonx/__init__.py`** — `pythonx` is an implicit namespace package, so several distributions +**no `pythonx/__init__.py`**, `pythonx` is an implicit namespace package, so several distributions can contribute subpackages under it. `pythonx/compose/` has `__init__.py`, `layout/`, `lite/`, `material3/`, `native/`, `runtime/`, `test/`, `ui/`, `ui/unit/`, `wrapper/`. @@ -358,7 +358,7 @@ both. Against the actual directories: | `pythonx.compose.runtime` | `androidx.compose.runtime` | yes | | `pythonx.compose.ui` | `androidx.compose.ui` | yes | | `pythonx.compose.ui.unit` | `androidx.compose.ui.unit` | yes | -| `pythonx.compose.layout` | `androidx.compose.foundation.layout` | **no — `foundation.` is dropped** | +| `pythonx.compose.layout` | `androidx.compose.foundation.layout` | **no, `foundation.` is dropped** | | `pythonx.compose.wrapper`, `.native`, `.lite`, `.test` | none | no counterpart | The `layout` entry is not inferred. `pythonx/compose/layout/arrangement.py` line 7 reads @@ -371,12 +371,12 @@ re-exports `Arrangement` from it. belongs to the Python package, not to the plugin.** The reason is ownership, not convenience. Which Kotlin package a `pythonx` module wraps is -`pythonx-compose`'s design decision — the plugin has no basis on which to invent +`pythonx-compose`'s design decision, the plugin has no basis on which to invent `pythonx.compose.layout` for `androidx.compose.foundation.layout`, and hard-coding Compose's particular renames into a general-purpose Gradle plugin would make every other library's mapping unreachable. -So: a data file inside the Python distribution — e.g. `pythonx/compose/pythonx-map.toml` — declaring +So: a data file inside the Python distribution, e.g. `pythonx/compose/pythonx-map.toml`, declaring [modules] "pythonx.compose.layout" = "androidx.compose.foundation.layout" @@ -408,14 +408,14 @@ description of anything. --- -## 6. Where the files go — the parts about the Pythonic product +## 6. Where the files go, the parts about the Pythonic product From §6.1 (measurements 3 and 4 stayed in `docs/design/pyi-generation-design.md`): Five mypy runs in `/Volumes/macMini/tmp/pyicheck`, each reported above or below: 1. **A `.pyi` beside a `.py` wins.** The fixture's `pythonx/compose/ui/__init__.py` contains nothing - but `def __getattr__(name): raise AttributeError(name)` — the shape the real adapter will have — + but `def __getattr__(name): raise AttributeError(name)`, the shape the real adapter will have, and every name in §4.4's table resolved from `__init__.pyi`. This is the deployment that matters: the dynamic adapter ships, the stub ships beside it, and the checker never sees the dynamism. 2. **`py.typed` is mandatory once the package is installed.** With `pythonx/` copied into the venv's @@ -426,13 +426,13 @@ Five mypy runs in `/Volumes/macMini/tmp/pyicheck`, each reported above or below: stubs or py.typed marker [import-untyped] note: Revealed type is "Any" (every line) - The marker goes in the top-level *regular* package of the distribution — here `pythonx/compose/`, + The marker goes in the top-level *regular* package of the distribution, here `pythonx/compose/`, because `pythonx` is a namespace package (§5.1). That placement was the one tested and it works. From §6.2: -- **For a published `pythonx` wheel the stubs belong inside the wheel** — `.pyi` beside `.py` plus - `py.typed`, both verified in §6.1 — built by `pypackpack`'s bundle stage, not regenerated per +- **For a published `pythonx` wheel the stubs belong inside the wheel**: `.pyi` beside `.py` plus + `py.typed`, both verified in §6.1, built by `pypackpack`'s bundle stage, not regenerated per consumer build. The plugin's per-build generation covers the consumer's own Kotlin and the third-party jars *its* build resolves; a shipped `pythonx-compose` covers Compose once. @@ -440,22 +440,22 @@ Two audiences, two destinations, one generator. --- -## 7. Open — the items that belonged to the Pythonic product +## 7. Open, the items that belonged to the Pythonic product - **PyCharm.** §4.4 rests entirely on mypy. Verify `ClassVar[Protocol]` resolution, `@overload` ordering, and `.pyi`-beside-`.py` precedence in PyCharm before implementing. If PyCharm does model metaclass data descriptors, §4.2's shape becomes viable again there and the two engines would want - different stubs — which would be a genuinely new problem. + different stubs, which would be a genuinely new problem. - **The 500-class stub cost.** ~507 callback Protocols for corpus A's `(receiver, name)` pairs, and 130 for `Modifier` alone in one file. Not measured for IDE indexing time or for mypy's own runtime. - **Whether any real Compose overload set collapses.** §3.6 shows four `padding` overloads coexisting, but two Kotlin overloads can map to identical Python signatures once `Dp | float` widening is applied. The generator must detect that and drop or qualify, and how often it happens across the - 33 multi-overload `Modifier` names is unknown — it needs the scan to be run. + 33 multi-overload `Modifier` names is unknown, it needs the scan to be run. - **Stub order vs dispatcher order.** §3.6: the `@overload` order and the Python-side dispatcher's resolution order must come from one place. The dispatcher does not exist (`docs/design/kotlin-extensions-in-python.md` §6). - *Archive note (2026-10-03): the dispatcher now exists — `_Overloads` in the binding layer + *Archive note (2026-10-03): the dispatcher now exists, `_Overloads` in the binding layer `python_multiplatform.binding` (`PythonxAdapter.kt`) serves the base name of an overload set.* - **klib.** §0. The `pythonx.*` surface on iOS and androidNative has no producer yet. - **The manifest of §5.3** does not exist and has not been agreed with `pythonx-compose`. diff --git a/docs/archive/pythonx-adapter-design.md b/docs/archive/pythonx-adapter-design.md index 2a41f03f3..7d2761e3f 100644 --- a/docs/archive/pythonx-adapter-design.md +++ b/docs/archive/pythonx-adapter-design.md @@ -1,11 +1,11 @@ -# `pythonx` — adapting generated Kotlin bindings without wrapping them one at a time +# `pythonx`, adapting generated Kotlin bindings without wrapping them one at a time > **Superseded** by [SPEC.md](../SPEC.md) U-7/U-8, > [`KotlinSurface.kt`](../../python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/KotlinSurface.kt) > and [`docs/design/kotlin-extensions-in-python.md`](../design/kotlin-extensions-in-python.md) on > 2026-10-03; kept for history. > -> **What is superseded:** the binder serving `pythonx` itself — it synthesised `pythonx.*` modules +> **What is superseded:** the binder serving `pythonx` itself, it synthesised `pythonx.*` modules > (with `__path__ = []`, so the real on-disk package could not load) and renamed members and > parameters to snake_case (§3's rule). Since `238119b7` the binder serves Kotlin-named modules under > Kotlin names only, with keyword arguments by Kotlin parameter name and Kotlin defaults; its layer is @@ -15,10 +15,10 @@ > Wherever this file says `pythonx._adapt`, `pythonx._bind_composable` or "the `pythonx` adapter", > read "the binding layer" for mechanics and "pythonx-compose" for naming. > -> **Still true, and where it lives now:** the binding-layer mechanics — lazy finder plus module +> **Still true, and where it lives now:** the binding-layer mechanics, lazy finder plus module > `__getattr__` (§2.3, §4.1), extension functions as receiver methods through a hybrid descriptor > (§4.2), the value-class allowlist (§4.4, now an empty runtime seam `allow_raw_primitive` that -> pythonx-compose fills), the composer stack and the `$default` mask computed per call (§5.6) — are +> pythonx-compose fills), the composer stack and the `$default` mask computed per call (§5.6), are > in `PythonxAdapter.kt`, whose KDoc still cites this file's section numbers. The `@Composable` > measurements (§5.2, §5.6) are summarised in `docs/design/kotlin-extensions-in-python.md` §2.3 and > §4.6 and pinned by `ComposableBindingTest` and `ksp-fixtures/compose`. The `Modifier` and material3 @@ -26,7 +26,7 @@ > naming conventions (§3) and the `Modifier` metaclass (§4.3) belong to pythonx-compose. The 2024 `pythonx-compose` wrote a Python class per Compose component. 37 files, 28 of them empty, -and the one that mattered most — `pythonx/compose/ui/modifier.py` — is a copy of the Button wrapper +and the one that mattered most, `pythonx/compose/ui/modifier.py`, is a copy of the Button wrapper in which `padding()` composes nothing and `fill_max_size()` returns `self`. The chain was never built. That is the cost this document exists to remove. @@ -53,7 +53,7 @@ Sources read for this document: --- -## 1. Read — what the 2024 code did, and the part of it that cannot have run +## 1. Read, what the 2024 code did, and the part of it that cannot have run Two conventions coexist in `pythonx-compose`, and they disagree. @@ -61,7 +61,7 @@ Two conventions coexist in `pythonx-compose`, and they disagree. `pythonx/compose/material3/text.py` declares `font_size`, `letter_spacing`, `on_text_layout`, `soft_wrap`, `max_lines`, `min_lines` and then rebuilds a kwargs dict spelling them `fontSize`, `letterSpacing`, `onTextLayout`, `softWrap`, `maxLines`, `minLines`. `buttons.py` does the same for -`on_click`, `content_padding`, `interaction_source`, five times over — the five button variants are +`on_click`, `content_padding`, `interaction_source`, five times over, the five button variants are five copies of one 60-line body differing only in the name they look up. **The notebook is not that code.** `docs/design/ecosystem.md` §3 established that `UI.ipynb` runs against @@ -77,7 +77,7 @@ self.__kotlin_composable(**kwargs, c=self.composer, changed=1) `c` and `changed` are the names PyREPL's `Material3.kt` gave its own explicit parameters. A real Compose composable's synthetic parameters are named `$composer` and `$changed`, which are not Python identifiers, so no keyword call can address them. Whatever `pythonx/compose/material3/*.py` was -tested against, it was not `androidx.compose.material3`. (Read, not run — no chaquopy runtime was +tested against, it was not `androidx.compose.material3`. (Read, not run, no chaquopy runtime was available here.) One more piece is worth keeping. `pythonx/compose/wrapper/__init__.py` is four lines: @@ -90,13 +90,13 @@ class KotlinWrapper: ``` It reclasses a live Java object onto a Python class to give it methods. That trick is chaquopy-only -and does not survive the move to this repository's binder — a Kotlin object reaches Python as an +and does not survive the move to this repository's binder, a Kotlin object reaches Python as an `ObjectReference` integer (`HandleTable`), and an `int` has no assignable `__class__`. Every place `alignment.py` and `arrangement.py` use it needs a different mechanism. --- -## 2. Read — what `PythonProxySource` already does, and what it does not +## 2. Read, what `PythonProxySource` already does, and what it does not ### 2.1 Injection into `sys.modules` is why no import hook is needed *after* `install()` @@ -105,13 +105,13 @@ name, and recursively creates and links its parents. CPython's import machinery `sys.modules` hit before it consults any finder, so `from fixture.library import greet` works with no `sys.meta_path` entry at all. -Entry names come from the producers as Kotlin fully-qualified names — `ArtifactScanner` +Entry names come from the producers as Kotlin fully-qualified names, `ArtifactScanner` builds `"$owner.${function.kotlinName}"`, where `owner` is the *package* for a file facade or multi-file part. So `androidx.compose.material3.Text` is the name a bound Compose composable would carry, and `import androidx.compose.material3` is what it makes importable. That is `docs/design/ecosystem.md` §5b's "a Kotlin FQN means the original Kotlin", already implemented. -### 2.2 Judged — what the `pythonx` prefix adds: nothing, and that is the point +### 2.2 Judged, what the `pythonx` prefix adds: nothing, and that is the point `pythonx` is an ordinary Python package. It does not need `sys.modules` injection, a finder, or any generation, because it is source we write. The two namespaces do not mix (§5b), and mechanically they @@ -120,14 +120,14 @@ do not even use the same publication route: `androidx.*` is injected, `pythonx.* What `pythonx` *does* need from `PythonProxySource` is that the Kotlin FQN it delegates to is reachable at the moment `pythonx` asks. Which is §2.3. -### 2.3 Judged — laziness needs two mechanisms, and the existing measurement does not forbid either +### 2.3 Judged, laziness needs two mechanisms, and the existing measurement does not forbid either `PythonProxySource.install()` is eager and whole-table: `render` walks every entry, emits one `_pm_h_N = _pm_lookup(...)` and one `def` per entry, and the caller `exec`s the lot. For the fixture tables this repository has, that is right. For Compose it is not: `docs/design/kotlin-extensions-in-python.md` §2.1 counts 1,411 public top-level functions in the desktop corpus alone, and §2.6 counts 500 public top-level composables. Every one would be a `_pm_resolve` string lookup and a Python function object -built before the first frame renders. (The install cost has **not** been measured — `GeneratedProxyCostTest` +built before the first frame renders. (The install cost has **not** been measured, `GeneratedProxyCostTest` prices per-call and per-read, not install. That measurement is missing.) Two different things have to become lazy, and they need different hooks: @@ -140,12 +140,12 @@ Two different things have to become lazy, and they need different hooks: **The 551–587 ns figure in `PythonProxySource`'s KDoc does not argue against this.** That measurement is about a `__getattr__` that answers *every* read of a live Kotlin property, where the raised-and-discarded `AttributeError` is paid per access. A lazy binder's `__getattr__` fires once, `setattr`s the result into the -module dict, and is never consulted for that name again — every later read is an ordinary dict hit. The +module dict, and is never consulted for that name again, every later read is an ordinary dict hit. The descriptor-versus-hook decision recorded there is about live properties and should not be transplanted. (Judged from the mechanism; the one-time cost of a lazy `__getattr__` on this repository's own boundary has not been measured.) -### 2.4 Read — the metadata the Python side cannot obtain +### 2.4 Read, the metadata the Python side cannot obtain This is the finding that constrains everything in §4. The two names a host must bind are `_pm_resolve(name_bytes) -> handle` and `_pm_invoke(handle, args_tuple) -> result`. That is the whole @@ -156,10 +156,10 @@ And `ExposedCallable` does not carry it either: | what an adapter needs | in `ExposedCallable`? | where it is lost | |---|---|---| -| parameter **names** | **no** — the class has `name`, `arity`, `paramTypes`, `returnType`, `kind`, `isSuspend`, `callable` | never collected; `kotlin-metadata-jvm` has `KmValueParameter.name`, KSP has it too | -| whether a parameter is an **extension receiver** | **no** | `ResolvedFunction.isExtension` exists and is used to pick the call syntax, then dropped — `ArtifactCallable` has no such field | +| parameter **names** | **no**, the class has `name`, `arity`, `paramTypes`, `returnType`, `kind`, `isSuspend`, `callable` | never collected; `kotlin-metadata-jvm` has `KmValueParameter.name`, KSP has it too | +| whether a parameter is an **extension receiver** | **no** | `ResolvedFunction.isExtension` exists and is used to pick the call syntax, then dropped, `ArtifactCallable` has no such field | | the receiver's **type** | **no** | same place | -| the declared **type name** of a parameter (`Dp` vs `Color`) | **no** — collapsed to a `TypeTag` | `resolveKotlinType` resolves a value class to its *underlying* `BoundaryType`, so a `Dp` parameter is `TypeTag.FLOAT` | +| the declared **type name** of a parameter (`Dp` vs `Color`) | **no**, collapsed to a `TypeTag` | `resolveKotlinType` resolves a value class to its *underlying* `BoundaryType`, so a `Dp` parameter is `TypeTag.FLOAT` | | whether a parameter **has a default** | **no** | `declaresDefaultValue` is readable from metadata and is not read | | the **overload set** for a name | **no** | `ArtifactScanner` drops a name entirely when more than one binding would carry it (`groupBy { it.name }.filterValues { it.size == 1 }`) | @@ -167,15 +167,15 @@ Consequences, in order of how much they hurt: 1. **`Text(text=..., color=...)` cannot be assembled.** Mapping keyword arguments onto positional slots requires parameter names, and nothing on either side of the boundary has them. This is not a - `pythonx` design choice — it is arithmetic. Without parameter names the Pythonic API is positional + `pythonx` design choice, it is arithmetic. Without parameter names the Pythonic API is positional only, which for a 16-parameter composable is not an API. 2. **"An extension function becomes a method on its receiver's proxy"** (`kotlin-extensions` §4.1) has no input. The table cannot say which entries are extensions or on what. 3. **The value-class allowlist** (`kotlin-extensions` §4.4) cannot be enforced in Python, because - Python cannot see that a parameter is a `Dp` rather than a `Float`. See §4.4 — it turns out the + Python cannot see that a parameter is a `Dp` rather than a `Float`. See §4.4, it turns out the allowlist mostly does not need to be enforced there. -### 2.5 Open — where `pythonx`'s own `.py` files live on iOS, androidNative and wasm +### 2.5 Open, where `pythonx`'s own `.py` files live on iOS, androidNative and wasm `PythonProxySource`'s KDoc gives, as its first reason for rendering Python at run time rather than shipping a `.py`, that **there is no resource path to put a `.py` on**: Kotlin/Native has no @@ -191,7 +191,7 @@ the Kotlin sources; the only hits are benchmarks reading `sys.path` as a test fi Candidates, none chosen and none tried: - `pypackpack`'s `bundle` type `resource`, which its SPEC defines as the handoff format to this - repository — and which `docs/design/ecosystem.md` §4 item 5 records as a three-line placeholder. + repository, and which `docs/design/ecosystem.md` §4 item 5 records as a three-line placeholder. - Freezing `pythonx` into the CPython trees the way the stdlib already is. - Embedding the source as Kotlin string constants and `exec`ing it, i.e. the route `PythonProxySource` took, applied to hand-written code. This works everywhere today and is @@ -202,7 +202,7 @@ Until one exists, `pythonx` is a desktop-and-Android library whatever else this --- -## 3. Measured — the name conventions `UI.ipynb` actually uses +## 3. Measured, the name conventions `UI.ipynb` actually uses Every identifier in the notebook's 57 cells, classified. This is the empirical basis for §4.1's rule, and it is worth reading before assuming "snake_case everything". @@ -218,18 +218,18 @@ and it is worth reading before assuming "snake_case everything". | `verticalArrangement`, `horizontalArrangement`, `horizontalAlignment` | `vertical_arrangement`, `horizontal_arrangement`, `horizontal_alignment` | parameter → snake_case | | `onClick` | `on_click` in `pythonx`, **`onclick`** in the notebook | parameter → snake_case; **`pythonx` is the reference, not the notebook** | | `State.getValue` / `setValue` | `getValue()` / `setValue()` | method on a Kotlin object → **camelCase kept** | -| — | `corner_radius`, `text_state` | **invented**; `material3.Card` has no `corner_radius` in either overload | +| - | `corner_radius`, `text_state` | **invented**; `material3.Card` has no `corner_radius` in either overload | Five Kotlin-derived multi-word parameters are snake_case; one (`onclick`) is not. The library source in `pythonx-compose` spells that same parameter `on_click` in all five button wrappers. **Decided: `pythonx` is the reference, and the notebook is not.** `on_click` is what the rule produces and what the library already writes, so `onclick` is not a spelling to support, alias or -carry forward — it is one cell of a teaching notebook disagreeing with the library it teaches. A +carry forward, it is one cell of a teaching notebook disagreeing with the library it teaches. A table derived from `pythonx` needs no exception list here, which is the point: an exception that exists only in prose is one the generator and the adapter cannot both honour. -**Judged — the rule.** +**Judged, the rule.** type, object, enum entry, composable PascalCase, unchanged function, method, property, parameter snake_case @@ -243,10 +243,10 @@ The composable exception is not a special case for Compose. A `@Composable` is P is a *declaration of a UI element*, and both Kotlin and the notebook treat it as a type-like name; PEP 8 has no opinion that outranks matching the documentation of the library being wrapped. The cost is that `pythonx` has two casing rules keyed on a property of the declaration, and the adapter must -know which — `@Composable` is an annotation the walker can read, so this is answerable, but it is one +know which, `@Composable` is an annotation the walker can read, so this is answerable, but it is one more thing the table would have to carry (§2.4). -**Judged — the conversion runs Kotlin → Python, and the runtime runs it backwards.** +**Judged, the conversion runs Kotlin → Python, and the runtime runs it backwards.** The `.pyi` generator (plugin, build time) converts Kotlin → Python: it has the Kotlin name and emits the Python one. The adapter (runtime) is handed the Python name by the caller and must find the @@ -267,12 +267,12 @@ an API the runtime does not have, which is the failure mode that makes stubs wor --- -## 4. Judged — the adaptation layer +## 4. Judged, the adaptation layer ### 4.1 What a module `__getattr__` looks up and what it caches ```python -# pythonx/_adapt.py — one implementation, no per-declaration code +# pythonx/_adapt.py, one implementation, no per-declaration code def __getattr__(name): # in pythonx/compose/material3/__init__.py kotlin = _kotlin_name(name, _MODULE) # 'androidx.compose.material3.Text' obj = _adapt(kotlin) # resolve + build the callable @@ -298,7 +298,7 @@ always available as an escape hatch. That is the property that keeps `pythonx` f extensions returns `Modifier`, so `Modifier.padding(16).background(...)` is ordinary Python method chaining with no combinator machinery. -The mechanism in Python is unremarkable — a proxy class per receiver type, and a `__getattr__` on +The mechanism in Python is unremarkable, a proxy class per receiver type, and a `__getattr__` on *its type* that resolves `receiver_type + '.' + kotlin_name`, binds, and installs the method on the class so the second instance pays nothing. @@ -313,7 +313,7 @@ constructs the call expression and discards them one line later. `kotlin-extensions` §4.2 established the shape and the reason: `Modifier$Companion` *implements* `androidx.compose.ui.Modifier`, so one Python class whose class object also behaves like an instance -covers all three spellings — `Modifier.padding(16)`, `m.padding(16)`, `def f(m: Modifier)`. +covers all three spellings, `Modifier.padding(16)`, `m.padding(16)`, `def f(m: Modifier)`. Two additions from reading the 2024 code: @@ -325,7 +325,7 @@ Two additions from reading the 2024 code: interface whose companion implements it. Whether any *other* Compose type has that shape was not counted. -### 4.4 Value classes — the coercion already happens, and it happens in Kotlin +### 4.4 Value classes, the coercion already happens, and it happens in Kotlin **Read.** `resolveKotlinType` (`KotlinMetadata.kt`) resolves a value-class parameter to the `BoundaryType` of its *underlying* type, and `valueClassBoundaryType` renders the read as @@ -334,28 +334,28 @@ already generated Kotlin, and `TypeTag` for a `Dp` parameter is `FLOAT`. This changes `kotlin-extensions` §4.4's conclusion less than it changes where it is enforced: -- **A raw Python number reaching a `Dp` parameter is already correct**, with no Python-side rule, and +- **A raw Python number reaching a `Dp` parameter is already correct**: with no Python-side rule, and `Modifier.padding(16)` needs nothing built for it. -- **`TextUnit` and every `packedValue` class is not a hazard on this path — it is unbound.** Their +- **`TextUnit` and every `packedValue` class is not a hazard on this path, it is unbound.** Their constructors are `INTERNAL` (`kotlin-extensions` §2.4), and `resolveKotlinType` returns `null` for a parameter whose value class has no public constructor. The silent-`Unspecified` failure §4.4 warns about cannot occur through a walked binding, because there is no walked binding. - **`Color` is unbound too, for a second reason.** Its constructor is public but takes `ULong`, and `kotlinPrimitiveBoundaryTypeOf` has no `kotlin/ULong` entry, so the underlying type resolves to `null`. The "constructor is public" test that §4.4 worried would admit `Color` does not admit it - here. (Read from source. Not re-run over Compose — `kotlin-extensions` §3 already reports the + here. (Read from source. Not re-run over Compose, `kotlin-extensions` §3 already reports the walker binds zero from these jars, for reasons upstream of this.) **Judged.** The allowlist stays a build-time concept, not a Python one. The Python side gets a -`dp(16)`-style proxy only where a *proxy object* is genuinely needed — that is, where a value class -appears in **return** position, or where a Python user wants to hold one — and unwraps it to the +`dp(16)`-style proxy only where a *proxy object* is genuinely needed, that is, where a value class +appears in **return** position, or where a Python user wants to hold one, and unwraps it to the underlying primitive before the call. The asymmetry §4.4 accepted (`padding(16)` fine, `font_size=16` rejected) arrives for free: `padding` binds and `Text`'s `fontSize` does not. -The residual risk §4.4 names — a future Compose release adding a public-constructor float wrapper -that packs — is unchanged and still lives at the plugin. +The residual risk §4.4 names, a future Compose release adding a public-constructor float wrapper +that packs, is unchanged and still lives at the plugin. -### 4.5 Open — defaults, and why they are the load-bearing blocker +### 4.5 Open, defaults, and why they are the load-bearing blocker **Measured** (`javap -p` over `material3-desktop-1.6.11.jar` and `foundation-layout-desktop-1.6.11.jar` from the Gradle cache, i.e. `kotlin-extensions-in-python.md`'s corpus A): @@ -371,7 +371,7 @@ from the Gradle cache, i.e. `kotlin-extensions-in-python.md`'s corpus A): parameters (58%) declare a default**, and 82 of the 177 functions have at least one. A Python caller writing `Text("hi", color=red)` is skipping fourteen parameters, and the values it -would have to supply instead are `TextStyle.Default`, `TextDecoration` and friends — objects the type +would have to supply instead are `TextStyle.Default`, `TextDecoration` and friends, objects the type gate does not bind, so Python cannot construct them to pass them. **"Supply every argument" is not available.** Defaults are therefore not an ergonomic nicety here; without them the bound surface is unusable even where it binds. @@ -382,17 +382,17 @@ unusable even where it binds. |---|---| | one entry per **arity prefix** (Kotlin defaults are trailing, so calling with the first *k* arguments is always legal) | *N*+1 entries instead of 2^*N*, and it cannot express "pass `text` and `font_size`, skip `modifier` and `color`", which is how Compose is written | | **exhaustive presence branching** in the generated lambda | 2^15 branches for `Text` | -| calling the compiler's **`$default` synthetic** from generated Kotlin | it is `ACC_SYNTHETIC`, so Kotlin source cannot name it; and `javap -p` over `ButtonKt` shows **no `Button$default` exists at all** — a `@Composable` carries its `$default` mask as a declared trailing parameter instead (§5.2), which is a different mechanism | +| calling the compiler's **`$default` synthetic** from generated Kotlin | it is `ACC_SYNTHETIC`, so Kotlin source cannot name it; and `javap -p` over `ButtonKt` shows **no `Button$default` exists at all**, a `@Composable` carries its `$default` mask as a declared trailing parameter instead (§5.2), which is a different mechanism | | a **generated Kotlin wrapper per function** that restates the defaults | the defaults are expressions in the callee's scope; metadata carries the *flag*, never the value (`kotlin-extensions` §4.5) | -The `@Composable` case may be luckier than the general case precisely because of §5.2 — the mask is a +The `@Composable` case may be luckier than the general case precisely because of §5.2, the mask is a real parameter there. Whether that is reachable is §5.4. --- ## 5. `@Composable` -### 5.1 Read — what the 2024 `Composable` class does +### 5.1 Read, what the 2024 `Composable` class does `pythonx/compose/runtime/__init__.py` is a decorator class holding a single `__composer` registered from Kotlin (`ComposeApp.__init__(composer)`), and `__call__` refuses to run if it is unset: @@ -410,17 +410,17 @@ through. `KotlinComposable.compose` is the interesting line: ComposableWrapper(self.content, args, self.composer, 1) ``` -`ComposableWrapper` is `io.github.thisisthepy.pycomposeui.RuntimeKt.composableWrapper` — a +`ComposableWrapper` is `io.github.thisisthepy.pycomposeui.RuntimeKt.composableWrapper`, a hand-written Kotlin function that takes a `ComposableLambda`, the argument list, a `Composer` **as an ordinary value**, and a `changed` int. That is the shape `docs/design/ecosystem.md` §5b describes as "the Python wrapper passes the composer as a value". So the 2024 design has exactly one composer for the whole application, set once, and every composable call uses it with `changed = 1`. Whether that produces correct recomposition was not verified here -and is not obviously true — `$changed` is a per-call-site bitmask about which arguments are known +and is not obviously true, `$changed` is a per-call-site bitmask about which arguments are known static, and a constant `1` tells the runtime something specific about slot 0. -### 5.2 Measured — the synthetic parameters, and a correction to `kotlin-extensions` §2.3 +### 5.2 Measured, the synthetic parameters, and a correction to `kotlin-extensions` §2.3 `javap -p`, corpus A jars, top-level parameter counts split at the top level (generics ignored): @@ -437,19 +437,19 @@ static, and a constant `1` tells the runtime something specific about slot 0. | `Row` | 4 | **7** | 6 | Every one of the nine rows is one higher than recorded. The trailing parameters of `Button` are -`Composer, int, int` and of `Text-fLXpl1I` are `Composer, int, int, int` — that is `$composer`, one or +`Composer, int, int` and of `Text-fLXpl1I` are `Composer, int, int, int`, that is `$composer`, one or two `$changed` masks, and **`$default`**. The distribution in §2.6 (`+2: 80, +3: 349, +4: 62, +5: 9`) was not re-derived; if it is shifted the same way, `+2` would mean "composable with no defaulted parameter" and the nine functions above would all be `+3` or `+4`, which is what was measured. The -design conclusion §4.6 draws — *never derive arity from the descriptor; metadata arity is the source* -— is unaffected and strengthened. +design conclusion §4.6 draws, *never derive arity from the descriptor; metadata arity is the source* +is unaffected and strengthened. Reproduce with: unzip -o -q "$GRADLE_CACHE/.../material3-desktop-1.6.11.jar" 'androidx/compose/material3/ButtonKt.class' javap -p androidx/compose/material3/ButtonKt.class -### 5.3 Read — the contradiction inside `docs/design/ecosystem.md` +### 5.3 Read, the contradiction inside `docs/design/ecosystem.md` §4 item 1 says: *"`@Composable` cannot be an exposed callable … a generated entry for a widget would not compile … This is the smallest and most blocking item."* @@ -462,7 +462,7 @@ the Kotlin side needs a `@Composable` callable type."* source* and let `kotlinc` compile it (agent-rules §12, and `ArtifactRendering.renderEntry` shows the generated lambda body verbatim). Kotlin source cannot: -- call a `@Composable` from a non-`@Composable` lambda — the Compose plugin rejects it; and +- call a `@Composable` from a non-`@Composable` lambda, the Compose plugin rejects it; and - name `$composer`, `$changed` or `$default`, which are not parameters of the Kotlin declaration at all. They exist only in the compiled form, which is the form §5b's argument reasons about and the form the generator does not emit. @@ -470,10 +470,10 @@ generated lambda body verbatim). Kotlin source cannot: Both facts are properties of the generator, not of Compose, which is why §5b's reading of the bytecode is correct and its conclusion still does not follow. The 2024 code got away with it because chaquopy calls *JVM methods by name and descriptor*, where the synthetic parameters are ordinary -arguments — the very approach `kotlin-extensions` §2.3 retires, since 108 mangled JVM names are +arguments, the very approach `kotlin-extensions` §2.3 retires, since 108 mangled JVM names are ambiguous within their own class. -### 5.4 Open — the one candidate that might reconcile them +### 5.4 Open, the one candidate that might reconcile them `androidx.compose.runtime.internal.composableLambdaInstance` builds a `ComposableLambda` from a `@Composable` block, and `ComposableLambda.invoke(p1, composer, changed)` is an ordinary @@ -489,12 +489,12 @@ private val bindButton = composableLambdaInstance(/* key */ 0, /* tracked */ tru ``` which would keep `ExposedCallable.callable` unchanged, let `kotlinc` supply `$composer`, `$changed` -**and `$default`** at a real call site — closing §4.5 for composables specifically — and let Python +**and `$default`** at a real call site, closing §4.5 for composables specifically, and let Python pass the composer as a value exactly as §5b says. **Not verified. Nothing here was compiled.** What has to be checked, in order: -1. That `composableLambdaInstance` is callable from generated code at all — it is in an `internal` +1. That `composableLambdaInstance` is callable from generated code at all, it is in an `internal` package and annotated `@ComposeCompilerApi`; whether that is an opt-in warning or a hard error for a consumer's generated source is unknown. 2. That the group key can be generated stably. Compose identifies slots by key; a colliding or @@ -510,7 +510,7 @@ pass the composer as a value exactly as §5b says. Until (1) is answered, `docs/design/ecosystem.md` §4 item 1 remains the blocking item it says it is, and §5b's paragraph asserting otherwise should be treated as unresolved rather than as settled. -### 5.5 Judged — arity is where the real risk sits, and it is not a collision risk +### 5.5 Judged, arity is where the real risk sits, and it is not a collision risk `kotlin-extensions` §2.6 measured that of 500 composables, 41 are extension functions and exactly one is a `Modifier` extension. Composables and chainable modifiers are two disjoint problems, so the @@ -518,12 +518,12 @@ adapter can treat them as two code paths with one shared name resolver and no ar them. The `Modifier`-extension composable (`ui.ExternalDrag_desktopKt.onExternalDrag`) is a single name and can be excluded by name if it is ever in the way. -### 5.6 Closed — Python calls a real composable, and §5.4's candidate was not what did it +### 5.6 Closed, Python calls a real composable, and §5.4's candidate was not what did it **`Text('hi')` written in Python draws pixels through `androidx.compose.material3.Text`.** `:ksp-fixtures:compose`'s `ComposableRenderTest` renders a 200×60 `ImageComposeScene`: the Python body drew **71** non-background pixels, an empty body drew **0**, and `Text('hi hi hi hi hi')` drew -**376**. Nothing is stubbed — `material3-desktop-1.6.11.jar` is walked, the composer is real, the +**376**. Nothing is stubbed, `material3-desktop-1.6.11.jar` is walked, the composer is real, the composition is real. Four things had to be settled, and three of them contradict what was written above. @@ -533,8 +533,8 @@ Four things had to be settled, and three of them contradict what was written abo It is not: `androidx.compose.runtime.Composable` is `@Retention(AnnotationRetention.BINARY)`, and `javap -v androidx/compose/material3/TextKt.class` shows every composable carrying `RuntimeInvisibleAnnotations: androidx.compose.runtime.Composable`. So the predicate answered -`false` for all 500 of them and `DeclarationModel.isComposable` — which drives §3.6's PascalCase -rule — had never once been true. It reads both lists now. +`false` for all 500 of them and `DeclarationModel.isComposable`, which drives §3.6's PascalCase +rule, had never once been true. It reads both lists now. **2. The trailing shape is derivable, and was checked rather than assumed.** §5.2 measured nine signatures by hand; `ComposableBindingTest.everyComposableJvmSignatureIsKotlinParamsThenComposerThenInts` @@ -546,7 +546,7 @@ contradicted it. **3. The mask encoding, read out of the callee.** `javap -c` on `Text-fLXpl1I` shows the prologue testing `$default & 2` before `modifier = Modifier.Companion`, `& 4` before -`color = Color.Unspecified`, `& 8` before the next — so **bit *i* is parameter *i***, with a bit +`color = Color.Unspecified`, `& 8` before the next, so **bit *i* is parameter *i***, with a bit reserved for every parameter including ones that default nothing (`text` owns bit 0 and nothing sets it). `$changed` is passed as **0**, the conservative value: it is a claim about argument staticness that this caller cannot make, and the callee's own `composer.changed(...)` branch is what runs when @@ -558,7 +558,7 @@ longer substituted reached Compose as `null`. **4. §5.4's `composableLambdaInstance` route was not needed, and a plainer obstacle than the one §5.3 named is what decided it.** §5.3 says generated Kotlin cannot name `$composer`, which is true. -But the *fallback* it assumes — call the JVM method directly — does not fail on the hyphen or on the +But the *fallback* it assumes, call the JVM method directly, does not fail on the hyphen or on the synthetic parameters. It fails earlier: probe.kt:2:35: error: unresolved reference 'TextKt'. @@ -568,25 +568,25 @@ real jar), so there is nothing to write. Java cannot spell `Text-fLXpl1I` either instead is **one `.class` per artefact fragment** (`ComposableThunks.kt`): a `public static Object t(Object[])` that unboxes the boundary's argument array and does one `INVOKESTATIC` with the name and descriptor copied verbatim from the `MethodNode` being walked. That -is not "looking a method up by name" in the sense agent-rules §12 retires — there is no runtime +is not "looking a method up by name" in the sense agent-rules §12 retires, there is no runtime lookup, no reflection, and the JVM's own linker resolves it at class-load time exactly as `kotlinc`'s own `INVOKESTATIC` would. So §4.5's table gains a row it did not have: **pass the mask as a value.** It is available because -§5.2's finding — the mask is a *declared* parameter — means it never had to be a compile-time +§5.2's finding, the mask is a *declared* parameter, means it never had to be a compile-time constant. Which arguments a call wrote is something Python knows, so the arithmetic is one integer per call in `pythonx._bind_composable`, for a declaration of any width. `MAX_OMITTABLE_PARAMETERS` and its 2^n cost apply to the sentinel mechanism and are untouched; a composable uses neither. **The composer still needs one hand-written `@Composable`**, and exactly one: `:ksp-fixtures:compose`'s `PythonComposition`. It names no composable and takes no -composable-specific parameter — it registers `currentComposer` as an ordinary object handle, pushes -it onto `pythonx`'s stack and runs the Python body — so it is O(1) in the number of bound +composable-specific parameter, it registers `currentComposer` as an ordinary object handle, pushes +it onto `pythonx`'s stack and runs the Python body, so it is O(1) in the number of bound composables, unlike 2024's wrapper-per-widget. `docs/design/ecosystem.md` §5b's "the Python wrapper passes the composer as a value" survives intact; §4 item 1's "this is the smallest and most blocking item" is closed. -**What is still open.** Recomposition and skipping (§5.4 item 4, §6) — the Python body is re-`exec`ed +**What is still open.** Recomposition and skipping (§5.4 item 4, §6), the Python body is re-`exec`ed on every composition pass and nothing has measured it. Extension composables are declined: Compose counts receivers in `$changed`'s slots but assigns `$default` bits over value parameters, and which numbering a receiver shifts has not been measured (§2.6 counts one `Modifier`-extension composable in @@ -595,18 +595,18 @@ exercise the second `$default` word, which the arithmetic writes and nothing has --- -## 6. Open — the lifetime of a Python callable across recomposition +## 6. Open, the lifetime of a Python callable across recomposition `docs/design/ecosystem.md` records this as unverified. This section says what "verify" means, because the mechanism turns out to be half-present already. -**Read — a Python callable can already cross into Kotlin.** `UpcallTrampoline.toKotlinObject` handles +**Read, a Python callable can already cross into Kotlin.** `UpcallTrampoline.toKotlinObject` handles `TypeTag.OBJECT` in the *parameter* direction by testing whether Python sent an integer: if it did, it is a `HandleTable` handle for a Kotlin object; if it did not, the value is wrapped as `PyObject(value, borrowed = true)` and the wrapper takes its own reference. So a `content=lambda: …` reaches a Kotlin parameter declared `PyObject` today, with a reference of its own. -**Read — the walker will not produce such a parameter.** `resolveKotlinType` has no `OBJECT` +**Read, the walker will not produce such a parameter.** `resolveKotlinType` has no `OBJECT` fallback, so an artefact-walked binding can never take one; only a KSP-generated entry from the consumer's own Kotlin can (KSP has `isPyObjectType`). A `content:` parameter on a walked `androidx.compose.material3.Button` is therefore unreachable regardless of everything else in §5. @@ -617,13 +617,13 @@ this repository has measured separately and never together: 1. **Release.** When Compose drops the slot, does anything tell the Kotlin holder? `RememberObserver.onForgotten` is the only hook that reports it. A `PyObject` held by an object that does not implement it is - leaked for the life of the process — the same class of defect + leaked for the life of the process, the same class of defect `ProxyHandleLifetimeTest` was written for, in the opposite direction (Kotlin object held by Python). A test of the same shape is needed: compose, drop, recompose, assert the Python refcount came back. 2. **The GIL on the release path.** `Py_DecRef` requires the GIL. `onForgotten` runs on whatever - thread Compose is applying changes on. Whether that thread holds the GIL — and if not, whether the - decref can be queued to one that does — is not answered anywhere in `docs/design/object-lifetime.md` or + thread Compose is applying changes on. Whether that thread holds the GIL, and if not, whether the + decref can be queued to one that does, is not answered anywhere in `docs/design/object-lifetime.md` or `docs/investigations/gil-parking-investigation.md` for this direction. 3. **Re-entrancy.** The call is Kotlin composition → Python callable → Kotlin composable → Python `content` lambda. The GIL is held across a Kotlin composition that may itself block or hop @@ -638,22 +638,22 @@ Item 1 is the one that fails silently and should be tested first. --- -## 7. Judged — what is Python code and what is a generated artefact +## 7. Judged, what is Python code and what is a generated artefact The dividing rule: **if it differs per Kotlin declaration it is generated or resolved at run time; if it is the same rule for every declaration it is `pythonx` Python source.** Everything the 2024 attempt -got wrong is on the wrong side of that line — a file per component is per-declaration code written by +got wrong is on the wrong side of that line, a file per component is per-declaration code written by hand. | product | who makes it | when | where it lives | consumer | |---|---|---|---|---| | `FunctionTableFragment` (consumer's own source) | KSP processor | build | generated Kotlin, compiled into the app | `UpcallTable` | | `ArtifactTable` fragments (resolved jars/klibs, AndroidX included) | Gradle plugin artefact walker | build | generated Kotlin, compiled into the app | `UpcallTable` | -| `.pyi` stubs, carrying the Pythonic names §3 produces | Gradle plugin (long term: `toolchain`) | build | `src//generated/meta/` | the IDE only — never imported | +| `.pyi` stubs, carrying the Pythonic names §3 produces | Gradle plugin (long term: `toolchain`) | build | `src//generated/meta/` | the IDE only, never imported | | proxy modules under Kotlin FQNs (`androidx.compose.material3`) | `PythonProxySource.render` | run time, `exec` | `sys.modules` | `pythonx`, and any user who wants the original | | `_pm_settle` / `_pm_watch` / `_pm_module` / `_pm_static_property` support | `PythonProxySource.support` | run time, `exec` | `__main__` | the generated proxies | | `_pm_resolve` / `_pm_invoke` | per-platform bootstrap (Kotlin/C) | run time | `__main__` | everything above | -| **`pythonx/`** — name conversion, module `__getattr__`, receiver-method attachment, the `Modifier` metaclass, value-class proxies, the composable call convention, the `content=` adapter | **written by hand, once** | — | a shipped Python package (§2.5: no route on 3 of 5 targets) | the application author | +| **`pythonx/`**, name conversion, module `__getattr__`, receiver-method attachment, the `Modifier` metaclass, value-class proxies, the composable call convention, the `content=` adapter | **written by hand, once** | - | a shipped Python package (§2.5: no route on 3 of 5 targets) | the application author | Two consequences worth stating explicitly. @@ -664,7 +664,7 @@ symptom to watch for. **`pythonx` may contain genuinely non-mechanical code, and that is where the line actually bends.** The notebook needs `main.App`, `App.update(NewComposable)` for hot-swapping a composable from a -Jupyter cell, and `App.messages` as live state. None of that is a mapping of a Kotlin declaration — +Jupyter cell, and `App.messages` as live state. None of that is a mapping of a Kotlin declaration, it is an application-lifecycle facility. It belongs in `pythonx` as hand-written Python (over a small hand-written Kotlin surface), and it should be kept visibly separate from the adapter so that "wrote a wrapper by hand" stays a visible event rather than a habit. @@ -691,14 +691,14 @@ Collected, in the order that unblocks the most. The argument for laziness is currently structural, not measured. 8. **`kotlin-extensions` §2.3's JVM parameter column** (§5.2) is one low on all nine rows; whether §2.6's `+2/+3/+4/+5` distribution shares the error was not re-derived. -9. **Scoped modifiers** — inherited unchanged from `kotlin-extensions` §6. 74 public member +9. **Scoped modifiers**: inherited unchanged from `kotlin-extensions` §6. 74 public member extensions on `Modifier` need a dispatch receiver Python has no implicit form for. No shape chosen. -10. **Overload dispatch** — inherited unchanged. The walker currently drops 33 of 130 `Modifier` +10. **Overload dispatch**: inherited unchanged. The walker currently drops 33 of 130 `Modifier` names. A Python-side dispatcher needs (1) to exist first, since it selects on argument names. --- -## 9. Measured — the `Modifier` extensions still declined, and `pointerInput` opened +## 9. Measured, the `Modifier` extensions still declined, and `pointerInput` opened `ArtifactScannerTest.composeModifierExtensionsSurviveBothGates` and `FunctionSlotBindingTest.mostFunctionTypedModifierExtensionsAreNoLongerDeclinedForBeingFunctionTyped` @@ -707,43 +707,43 @@ were run against the real Compose 1.7.0 desktop jars (`ANDROID_HOME=... ./gradle by name rather than assume the count in either test's own KDoc is still current. It is not: **nine** public top-level `Modifier` extensions have zero binding overload today, not the "44"/"8" figures recorded when those tests were written. One more name (`pullRefresh`) has -a declined *overload* beside a bound one and so is not in this list — a caller can already reach +a declined *overload* beside a bound one and so is not in this list, a caller can already reach `pullRefresh(modifier, state, enabled)`, just not the callback-based overload. | name | JVM signature (`javap -p`, Compose 1.7.0) | decline reason (`ArtifactScanner`, measured) | |---|---|---| -| `pointerInput` | `Modifier.pointerInput(Any?, suspend PointerInputScope.() -> Unit)` → `Function2, Any?>` | suspend function-typed parameter — **now bound**, see below | +| `pointerInput` | `Modifier.pointerInput(Any?, suspend PointerInputScope.() -> Unit)` → `Function2, Any?>` | suspend function-typed parameter, **now bound**, see below | | `dragAndDropSource` | `dragAndDropSource(Modifier, (DrawScope)->Unit, suspend DragAndDropSourceScope.() -> Unit)` → `Function2<..., Continuation, Any?>` | suspend function-typed parameter, same family as `pointerInput` | | `draggable` | `draggable(Modifier, DraggableState, ..., onDragStarted: suspend CoroutineScope.(Offset)->Unit, onDragStopped: suspend CoroutineScope.(Float)->Unit, ...)` → both `Function3, Any?>` | suspend function-typed parameter | | `composed` | `composed(Modifier, (InspectorInfo)->Unit, factory: @Composable Modifier.() -> Modifier)` → `factory` compiles to `Function3` | `@Composable` function-typed parameter of a declaration that is not itself `@Composable`: `ArtifactScanner`'s generated fragment is compiled without the Compose plugin | | `swipeable` | `swipeable-pPrIpRY(Modifier, SwipeableState, ..., thresholds: (T, T) -> ThresholdConfig, ...)` | the `thresholds` slot's declared type argument is the type parameter `T`, which has no name a cast could spell | | `modifierLocalProvider` | `modifierLocalProvider(Modifier, ProvidableModifierLocal, () -> T)` | same: `() -> T`'s `T` has no name | -| `anchoredDraggable` | `anchoredDraggable(Modifier, AnchoredDraggableState, ...)` | not function-typed at all — the *required* `state` parameter's own type carries the unnameable `T`, so `resolveKotlinType` answers "no boundary type" before the function-slot grammar is ever asked | -| `layoutId` | `layoutId(Modifier, layoutId: Any)` | not function-typed — `resolveKotlinType` has no boundary type for a bare `kotlin.Any`/`java.lang.Object` parameter (no hint of which `TypeTag` an arbitrary Python value crossing there should marshal as) | -| `contextMenuOpenDetector` | `contextMenuOpenDetector(Modifier, key: Any?, enabled: Boolean, onOpen: (Offset)->Unit)` | not function-typed — same as `layoutId`, `resolveKotlinType` has no boundary type for `kotlin.Any` | +| `anchoredDraggable` | `anchoredDraggable(Modifier, AnchoredDraggableState, ...)` | not function-typed at all, the *required* `state` parameter's own type carries the unnameable `T`, so `resolveKotlinType` answers "no boundary type" before the function-slot grammar is ever asked | +| `layoutId` | `layoutId(Modifier, layoutId: Any)` | not function-typed, `resolveKotlinType` has no boundary type for a bare `kotlin.Any`/`java.lang.Object` parameter (no hint of which `TypeTag` an arbitrary Python value crossing there should marshal as) | +| `contextMenuOpenDetector` | `contextMenuOpenDetector(Modifier, key: Any?, enabled: Boolean, onOpen: (Offset)->Unit)` | not function-typed, same as `layoutId`, `resolveKotlinType` has no boundary type for `kotlin.Any` | So the nine split into three unrelated limits, not one: - **suspend lambda** (`pointerInput`, `dragAndDropSource`, `draggable`, and `pullRefresh`'s declined - overload) — §9.1 opens one of these. + overload), §9.1 opens one of these. - **generic type parameter with no spellable name** (`swipeable`, `modifierLocalProvider`, - `anchoredDraggable`) — a walker limit, not a suspend one; see §9.2. -- **compile-model mismatch** (`composed`) and **unconstrained parameter type** (`layoutId`, `contextMenuOpenDetector`) — see §9.2. + `anchoredDraggable`), a walker limit, not a suspend one; see §9.2. +- **compile-model mismatch** (`composed`) and **unconstrained parameter type** (`layoutId`, `contextMenuOpenDetector`), see §9.2. -### 9.1 `Modifier.pointerInput` — judged reachable, and reached +### 9.1 `Modifier.pointerInput`, judged reachable, and reached The blocking claim in `FunctionSlotBindingTest`'s KDoc is narrower than it reads: "a Python callable cannot answer `COROUTINE_SUSPENDED`" is true of a *bare* Python callable substituted for the whole -suspend lambda, but `pointerInput`'s block does not need to *be* the suspend function — it needs to +suspend lambda, but `pointerInput`'s block does not need to *be* the suspend function, it needs to *call into* one, once per event, and the suspension all happens inside `awaitPointerEventScope { awaitPointerEvent() }`, which is ordinary Kotlin the compiler already knows how to build a state machine for. Nothing about Python has to suspend at all if the loop that suspends is hand-written -Kotlin and Python is only ever asked a synchronous question at each iteration — exactly the shape +Kotlin and Python is only ever asked a synchronous question at each iteration, exactly the shape `PythonCallables.PythonFunction` already crosses for every *other* `Function0..Function5` slot in this codebase (§4.2's `content=` in this file, among others). That is `fixture.compose.pythonPointerInput` (`ksp-fixtures/compose/src/desktopMain/kotlin/fixture/ -compose/PythonPointerInput.kt`): a hand-written, ordinarily-compiled `suspend` Kotlin function — +compose/PythonPointerInput.kt`): a hand-written, ordinarily-compiled `suspend` Kotlin function, fun pythonPointerInput(modifier: Modifier, onEvent: PyObject): Modifier = modifier.pointerInput(Unit) { @@ -758,7 +758,7 @@ compose/PythonPointerInput.kt`): a hand-written, ordinarily-compiled `suspend` K } bound by KSP exactly the way `fixture.compose.emptyModifier` already is (`FragmentScanner -.topLevelFunctionEntry`, a plain top-level function, no artefact walker involved — `Modifier. +.topLevelFunctionEntry`, a plain top-level function, no artefact walker involved, `Modifier. pointerInput` itself is untouched and still declines, correctly, for the reason in the table above). It is a plain parameter rather than an extension receiver on purpose: `FragmentScanner.topLevelFunctionEntry` calls a declaration by its qualified name over @@ -769,28 +769,28 @@ shape that works today. **Proven, not asserted.** `ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ PointerInputRenderTest.kt` drives a real `Move`/`Press`/`Release` sequence through -`ImageComposeScene.sendPointerEvent` — the same entry point `CallbackDrivenRenderTest` uses for -`Checkbox` — at a `Text` sized by the walked `size__Dp`, whose displayed string is +`ImageComposeScene.sendPointerEvent`, the same entry point `CallbackDrivenRenderTest` uses for +`Checkbox`, at a `Text` sized by the walked `size__Dp`, whose displayed string is `str(len(_tap_events))`. The test asserts, in Python, that at least one `press` event arrived with coordinates inside the 48×48 target box (not a stub value), and separately that a *fresh* scene shows -a different digit once the callback has run — the same two-part shape (`_events` list, then a second +a different digit once the callback has run, the same two-part shape (`_events` list, then a second scene) `CallbackDrivenRenderTest` uses for `Checkbox`/`Switch`, for the same reason: the scene that received the tap does not itself recompose from a Python list mutation Compose's snapshot system cannot see. Confirmed red first: with `PythonPointerInput.kt` removed, both tests fail with `PyException: cannot import name 'pythonPointerInput' from 'fixture.compose'`; restored, both pass (`:ksp-fixtures:compose:desktopTest`, 50 tests / 0 failures, up from 48 before this file). -### 9.2 `draggable` and `dragAndDropSource` — reached, the same way `pointerInput` was +### 9.2 `draggable` and `dragAndDropSource`, reached, the same way `pointerInput` was -Both measured against the real Compose 1.7.0 desktop jars (`javap -p`, and — since there is no sources -jar for this Compose version — bytecode disassembly of the internal node classes where the declared +Both measured against the real Compose 1.7.0 desktop jars (`javap -p`, and, since there is no sources +jar for this Compose version, bytecode disassembly of the internal node classes where the declared signature alone did not settle how the slot is actually driven), 2026-08-17. - **`draggable`** (`androidx.compose.foundation.gestures.DraggableKt.draggable`) is not one suspend slot but two different shapes: `state: DraggableState`'s own `onDelta` (built by the top-level `DraggableState(onDelta: (Float) -> Unit): DraggableState` factory, itself a plain function, not `@Composable`) is an *ordinary, non-suspend* `Function1` the internal drag node calls - directly, once per raw pointer delta — the same shape every other plain callback in this codebase + directly, once per raw pointer delta, the same shape every other plain callback in this codebase already crosses. `onDragStarted: suspend CoroutineScope.(Offset) -> Unit` and `onDragStopped: suspend CoroutineScope.(Float) -> Unit` are `draggable`'s own parameters and are the `pointerInput` shape: each invoked once per gesture, by a coroutine whose body never actually @@ -799,7 +799,7 @@ signature alone did not settle how the slot is actually driven), 2026-08-17. `DraggableState` from one `PyObject` callback and passes two hand-written trivial-suspend lambdas for the other two, calling all three synchronously. **Lifetime is not solved here and the file says so**: unlike `pointerInput`'s single suspend loop, there is no one coroutine whose `finally` covers this - function's whole lifetime — `onDelta` is held by the `DraggableState` for as long as the internal drag + function's whole lifetime, `onDelta` is held by the `DraggableState` for as long as the internal drag node stays attached, which this function has no hook into without wrapping itself in a `@Composable` (a different fix; see `composed` below). The three `PyObject` references are never closed; each call to `pythonDraggable` leaks them. Measured, not guessed: `DraggableRenderTest` composes it once per @@ -819,17 +819,17 @@ signature alone did not settle how the slot is actually driven), 2026-08-17. - **`dragAndDropSource`** (`androidx.compose.foundation.draganddrop.DragAndDropSourceKt .dragAndDropSource`) turns out to be the *same* mechanism `pointerInput` uses, not merely the same declared shape: disassembling `DragAndDropSourceNode`'s constructor shows it `delegate`s a - `SuspendingPointerInputFilterKt.SuspendingPointerInputModifierNode(dragAndDropSourceHandler)` — the + `SuspendingPointerInputFilterKt.SuspendingPointerInputModifierNode(dragAndDropSourceHandler)`, the identical node `Modifier.pointerInput` itself delegates to. `DragAndDropSourceScope` (the `block` parameter's receiver) extends `PointerInputScope`, so `fixture.compose.pythonDragAndDropSource`'s body - is `pythonPointerInput`'s loop, verbatim, on a different receiver type — including the same + is `pythonPointerInput`'s loop, verbatim, on a different receiver type, including the same `finally`-based lifetime, since it is the same underlying coroutine. `drawDragDecoration` (the other, non-suspend parameter) is a hardcoded no-op here; this proves the suspend slot's events reach Python, not that `startTransfer`/`drawDragDecoration` do anything real (a `DragAndDropTransferData` needs a `java.awt.datatransfer.Transferable`, a real drag payload and a different, unmeasured claim). Required `@OptIn(ExperimentalFoundationApi::class)`, and one thing surfaced only at runtime, not in `javap`: Compose Multiplatform desktop prints `"Compose Multiplatform doesn't support Modifier - .dragAndDropSource yet"` (once per composition of the modifier) — a warning from elsewhere in + .dragAndDropSource yet"` (once per composition of the modifier), a warning from elsewhere in `dragAndDropSource`'s own implementation, not an exception, and it does not stop the delegated `SuspendingPointerInputModifierNode` from receiving real events: the render test's ink still moves (58→59px, 68px changed) and Python's assertions on real (kind, x, y) values still pass. @@ -839,23 +839,23 @@ signature alone did not settle how the slot is actually driven), 2026-08-17. Confirmed red first (`cannot import name 'pythonDragAndDropSource'` with the file removed); green restored. -### 9.3 `layoutId` — reached, and one more limit found along the way +### 9.3 `layoutId`, reached, and one more limit found along the way `layoutId(Modifier, id: Any)` declines for the reason §9's table gives: its one non-`Modifier` parameter is `kotlin.Any`, which `resolveKotlinType` has no `TypeTag` for. `fixture.compose .pythonLayoutIdString(modifier: Modifier, id: String): Modifier` fixes the parameter's tag exactly the -way the table's KDoc predicted — one concrete-`String` wrapper, no suspend or generic-type problem +way the table's KDoc predicted, one concrete-`String` wrapper, no suspend or generic-type problem involved at all. Proving what it tagged is where a second, previously unmeasured limit showed up. A first version paired it with a hand-written `@Composable fun pythonLayoutIdProbe(...)`, meant to read `Measurable.layoutId` -back and place a child accordingly — bound as a plain top-level function the way `pythonPointerInput` +back and place a child accordingly, bound as a plain top-level function the way `pythonPointerInput` is. **It does not bind, confirmed by running it**: `cannot import name 'pythonLayoutIdProbe'`, silently -(no compile error, no KSP warning surfaced by the "e: "-line check) — `BindingPolicy.isComposable` +(no compile error, no KSP warning surfaced by the "e: "-line check), `BindingPolicy.isComposable` excludes *every* `@Composable` top-level function from `FragmentScanner`'s plain-function path on purpose, per its own KDoc: a `@Composable` in the consumer's own source cannot be called from the generated, non-composable fragment source, and KSP must not try. `PythonComposition` is not a -counterexample to this — Python never calls it; `ImageComposeScene`'s own composable content lambda +counterexample to this, Python never calls it; `ImageComposeScene`'s own composable content lambda does, from a context that already has a composer. This is a more definite answer than `composed`'s "unverified whether a composer is admitted at all" below: for a plain hand-written composable in this module's own source, the answer is no, independent of any per-declaration contract question. @@ -866,38 +866,38 @@ whatever single `Text` Python's body composed. It places that child at x=0 if th equals an `expectTag` known only to the Kotlin test (never crossed to Python) or at x=60 otherwise. Positive case (`aMatchingLayoutIdPlacesTheChildAtTheLeft`): tag and probe both `"python-tag"`, ink at x∈[0,20). Negative control (`aMismatchedLayoutIdPlacesTheChildAtTheRight`): tag `"python-tag"`, probe -`"a-different-tag"`, ink at x∈[60,80) instead — ruling out a stub that always reports a match or drops +`"a-different-tag"`, ink at x∈[60,80) instead, ruling out a stub that always reports a match or drops the string Python actually sent. Confirmed red first (`cannot import name 'pythonLayoutIdString'` with `PythonLayoutId.kt` removed); green restored. `pythonLayoutIdInt` was judged not worth building alongside it: the `String` case is what needed a second crossing path to be found at all, and `Int` would exercise nothing new. -### 9.4 `pullRefresh`'s callback overload and the generic-type group — still not attempted; `composed` — reached +### 9.4 `pullRefresh`'s callback overload and the generic-type group, still not attempted; `composed`, reached - **`pullRefresh`'s callback overload** (`androidx.compose.material.pullrefresh.PullRefreshKt .pullRefresh(Modifier, onPull: (Float) -> Float, onRelease: suspend (Float) -> Float, enabled: Boolean)`) turns out **not** to be the same shape §9's table assumed. Disassembling it (no sources jar for this Compose version) shows it builds a `PullRefreshNestedScrollConnection(onPull, onRelease, - enabled)` and installs it with `Modifier.nestedScroll` — `onPull`/`onRelease` are driven by + enabled)` and installs it with `Modifier.nestedScroll`, `onPull`/`onRelease` are driven by `NestedScrollConnection` callbacks from a *scrollable descendant's* pre-scroll/pre-fling reports, not by pointer events this modifier's own suspend body awaits directly the way `pointerInput`, `draggable` and `dragAndDropSource` all do. `onPull` also has to *return* the `Float` amount consumed, which `pythonDraggable`'s `onDelta` (returning `Unit`) does not. Reaching it the way §9.2 reaches - `draggable` is very likely still possible — `onPull`'s marshalling is one more step than a `Unit` - callback, and `onRelease` is the same trivial-suspend-lambda shape as `onDragStopped` — but proving it + `draggable` is very likely still possible, `onPull`'s marshalling is one more step than a `Unit` + callback, and `onRelease` is the same trivial-suspend-lambda shape as `onDragStopped`, but proving it needs a real nested-scroll parent/child composed inside the test (a scrollable container whose over-scroll at rest actually reaches `PullRefreshNestedScrollConnection`), which is a materially larger test harness than a bare tagged `Modifier` under a pointer. Not attempted here for that reason, not a suspend-shape reason. -- **`composed`** — **attempted, reached, and proven.** Its `factory` compiles to the *same* lowered +- **`composed`**: **attempted, reached, and proven.** Its `factory` compiles to the *same* lowered shape (`Function3`, confirmed by `javap -p androidx.compose.ui.ComposedModifierKt`) `PythonCallables` already crosses for every composable - `content=` slot, so the missing piece was never the crossing but a call site — which + `content=` slot, so the missing piece was never the crossing but a call site, which `ksp-fixtures/compose` already has the Compose plugin to compile. The verdict this section left open - — "still unverified whether `composed`'s own contract... admits a composer at all" — is answered + , "still unverified whether `composed`'s own contract... admits a composer at all", is answered **yes**, and the premise in that sentence was backwards: `composed` itself is not `@Composable` (the same `javap` output shows no synthetic `$composer`/`$changed` on the `composed` static method), which - does not make a composer *unavailable* to `factory` — it means `BindingPolicy.isComposable` (which + does not make a composer *unavailable* to `factory`, it means `BindingPolicy.isComposable` (which excludes only a declaration *itself* annotated `@Composable`) never sees `pythonComposed` and does not exclude it, exactly as this paragraph predicted. `fixture.compose.pythonComposed` (`ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposed.kt`) is a plain, @@ -905,12 +905,12 @@ would exercise nothing new. composable-typed lambda literal is where `currentComposer` resolves, exactly as `PythonComposition`'s does. What "admits a composer" turned out to mean, and what needed proving rather than inferring from bytecode, is whether that composer is wired into a *real, positionally-stable slot table* rather than - a placeholder that merely satisfies the type — `ComposedRenderTest + a placeholder that merely satisfies the type, `ComposedRenderTest .theSameCompositionReusesTheRememberedSlotAcrossRecompositions` measures it: a `remember { }` inside the `composed { }` lambda allocates once (`remembersAllocated == 1`) and is *reused* across six forced recompositions of the same node, even though `PythonComposition` re-`exec`s the Python source on every pass and so calls `pythonComposed` itself afresh each time, building a brand-new - `ComposedModifier` Kotlin object — the slot survives because `Modifier.composed`'s own + `ComposedModifier` Kotlin object, the slot survives because `Modifier.composed`'s own materialize-time group is positional, not object-identity-based, which is the mechanism that lets a real `Modifier.clickable`'s internal `remember(interactionSource) { ... }` survive a modifier chain rebuilt from scratch every recomposition. A placeholder composer could not have produced that @@ -918,16 +918,16 @@ would exercise nothing new. proof that the `Modifier` `factory` returns is the one actually measured, not a stub: a Python `factory` calling the walked `size__Dp(receiver, 70.0)` measures a 70px-wide placeable, a second one with `33.0` measures 33px, and a negative-control identity factory (`return receiver`, no resize) - measures 8px (`Text("x")`'s own intrinsic width) — ruling out both "the result is ignored" and "any + measures 8px (`Text("x")`'s own intrinsic width), ruling out both "the result is ignored" and "any factory produces the same fixed size." Not solved: `factory`'s one Python reference is captured by the closure and never released (no `RememberObserver` hook exists for a bare captured `PyObject` the - way `content=`'s `PythonCallableScope` has one) — the same documented, bounded shape as + way `content=`'s `PythonCallableScope` has one), the same documented, bounded shape as `pythonDraggable`'s leak above, out of scope for this question. - **`swipeable`, `modifierLocalProvider`, `anchoredDraggable`** share a limit `pythonPointerInput`'s technique does not reach: the unspellable part is a **type parameter** (`T` in `SwipeableState`, `ProvidableModifierLocal`, `AnchoredDraggableState`), not a suspend modifier. A hand-written wrapper can dodge this the same way `PyObject`-typed parameters dodge every other type problem at - this boundary, but only per concrete `T` — `pythonSwipeableFloat(modifier, state: + this boundary, but only per concrete `T`, `pythonSwipeableFloat(modifier, state: SwipeableState, ...)`, one declaration per instantiation a caller wants, not one wrapper that covers the whole generic surface the way `pythonPointerInput` covers every `pointerInput` call site. Not attempted, and not the same shape of fix. diff --git a/docs/archive/threading-and-abi-3.15t-gate.md b/docs/archive/threading-and-abi-3.15t-gate.md index aad63ff2b..a95c28c53 100644 --- a/docs/archive/threading-and-abi-3.15t-gate.md +++ b/docs/archive/threading-and-abi-3.15t-gate.md @@ -9,10 +9,10 @@ Free-threaded builds **do not support the Limited API or the Stable ABI** in 3.13 or 3.14. All ~330 bindings here target the Stable ABI, so a free-threaded 3.14 build cannot be used without abandoning -it and recompiling per Python version — which would defeat the version-parameterised acquisition +it and recompiling per Python version, which would defeat the version-parameterised acquisition pipeline. -[PEP 803](https://peps.python.org/pep-0803/) — **Final**, targeting 3.15 — introduces `abi3t`, a +[PEP 803](https://peps.python.org/pep-0803/), **Final**, targeting 3.15, introduces `abi3t`, a Stable ABI variant for free-threaded builds, with `abi3t` / `abi3.abi3t` wheel tags and forward compatibility across all later versions. 3.15 is therefore the first release where free-threading and the Stable ABI coexist. @@ -22,7 +22,7 @@ and the Stable ABI coexist. | Requirement | Our status | |---|---| | `PyObject` / `PyVarObject` become incomplete types; no field access | **Compatible.** Verified: `PyObject` appears only as `CPointer` and is never dereferenced. The only `.pointed` uses in the tree are commented-out `JNIEnv` code in `JniExport.kt`. | -| Extensions may not embed `PyObject` in their own structs | Compatible — nothing does. | +| Extensions may not embed `PyObject` in their own structs | Compatible, nothing does. | | `PyModExport` hook (PEP 793) instead of static `PyModuleDef` | Not yet relevant. We embed CPython rather than building an extension module. **It becomes relevant for the upcall work**, where Kotlin classes are exposed to Python. | | No backwards compatibility with 3.14 or earlier | Was accepted at the time; no longer applies, 3.14t is used on desktop (ROADMAP §9). | @@ -39,7 +39,7 @@ and the Stable ABI coexist. Desktop free-threaded builds exist today, including 3.15 release candidates. **Mobile has none**, so enabling free-threading before mobile artefacts appear would split the threading model across -platforms — and with it the object-lifetime and thread-state design layered on top. +platforms, and with it the object-lifetime and thread-state design layered on top. Switching the default (originally) required all three of the following; ROADMAP §9 shows 1 is not needed for 3.14t on desktop, while 2 still holds: diff --git a/docs/archive/wasm-design-experiment-log.md b/docs/archive/wasm-design-experiment-log.md index e78d3d494..132c2a6c8 100644 --- a/docs/archive/wasm-design-experiment-log.md +++ b/docs/archive/wasm-design-experiment-log.md @@ -2,7 +2,7 @@ > This is the chronological lab notebook of the wasm experiment (feasibility analysis, "Why WASM is > parked", then a series of corrections as each hypothesis was measured). Its early sections say wasm > is **parked / not implemented / blocked on Kotlin's toolchain**, that CPython is pinned to 3.13 and -> Kotlin to 2.0.20, and that a C shim and an `EXPORTED_FUNCTIONS` list are needed — **all of that was +> Kotlin to 2.0.20, and that a C shim and an `EXPORTED_FUNCTIONS` list are needed, **all of that was > later disproved or overtaken**; read the sections in order and treat any "unverified" or "parked" > claim as superseded by the sections after it and by the integrated implementation > (`python-multiplatform/src/wasmJsMain/`, `wasmJsTest/`, `tools/wasm/build-cpython.sh`; SPEC §0, C-6, M-3, U-5). @@ -67,7 +67,7 @@ Currently, Kotlin/Wasm cannot dynamically link directly to an arbitrary C WebAss ## 7. Recommendation and Staged Plan -**Recommendation:** Pursue **Approach (b) — Raw C Symbols via JS Bridge** for the `wasmJs` target. It is the only path that preserves the multiplatform object model. Ignore `wasmWasi` until Wasm Component Model linking matures in Kotlin. +**Recommendation:** Pursue **Approach (b), Raw C Symbols via JS Bridge** for the `wasmJs` target. It is the only path that preserves the multiplatform object model. Ignore `wasmWasi` until Wasm Component Model linking matures in Kotlin. **Caveat:** WASM will have permanently higher per-call overhead than Native/JVM due to the WasmGC ↔ JS ↔ Emscripten boundary crossings. @@ -87,7 +87,7 @@ The analysis above assumed Kotlin/Wasm is the only route. That is true today, bu fundamental reason. Kotlin/Native used to ship a `wasm32` target built through LLVM, producing linear-memory -WebAssembly with a C ABI — the same memory model an Emscripten build of CPython uses. That target +WebAssembly with a C ABI, the same memory model an Emscripten build of CPython uses. That target could in principle have used `cinterop`, exactly as the iOS target does today. JetBrains **deprecated it in Kotlin 1.8.20 and removed it in 1.9.20**, in favour of the Kotlin/Wasm toolchain, which skips LLVM and is built on WasmGC. @@ -98,7 +98,7 @@ The consequence is that the JS bridge is forced by *tooling*, not by WebAssembly - Kotlin/Wasm has no `cinterop` at all. `wasmJs` offers JS interop; `wasmWasi` offers WASI syscalls. This project targets Kotlin 2.0.20, so the `wasm32` target is long gone and pinning to 1.9.10 is -not viable — the rest of the toolchain requires 2.0.x. +not viable, the rest of the toolchain requires 2.0.x. ### The dependency is CPython's own WASM support, not Pyodide @@ -121,7 +121,7 @@ Tier 3. Three of its details bear directly on this design: dynamic builds. CPython therefore cannot be dynamically loaded into another WebAssembly module. 2. **The build produces an `.mjs` plus `.wasm` pair.** A JavaScript runtime layer is intrinsic to the Emscripten build; it is not something an integration can route around. -3. **No official python.org distribution is mandated** — binaries continue to come from downstream, +3. **No official python.org distribution is mandated**: binaries continue to come from downstream, i.e. Pyodide. Avoiding Pyodide as a *dependency* therefore means building CPython from source ourselves, not that an upstream binary exists to use instead. @@ -148,20 +148,20 @@ Recorded so the reasoning survives. Nothing here is implemented. ## The blocker is Kotlin's toolchain, not WebAssembly -Every other platform calls CPython directly — cinterop on iOS and androidNative, Panama on +Every other platform calls CPython directly, cinterop on iOS and androidNative, Panama on desktop, JNI on Android. Kotlin/Wasm cannot: `wasmJs` offers only JS interop and `wasmWasi` only WASI syscalls, and neither can link a C library. Kotlin/Native once had a `wasm32` target that went through LLVM and emitted linear-memory -wasm — the same memory model Emscripten's CPython uses, which is exactly what would have made +wasm, the same memory model Emscripten's CPython uses, which is exactly what would have made cinterop work. It was deprecated in 1.8.20 and removed in 1.9.20. So the JS bridge is a consequence of a toolchain decision, not of WebAssembly. **The deeper barrier is memory, not calls.** Two wasm modules can call each other directly if -the host wires module B's exports to module A's imports at instantiation — JS is needed once, +the host wires module B's exports to module A's imports at instantiation, JS is needed once, not per call. But Kotlin/Wasm is WasmGC: its objects live on a managed heap, and it has no way to read another module's linear memory. CPython's entire API is pointers into exactly that -memory. So JS is not a convenience in the call path — it is where the marshalling has to +memory. So JS is not a convenience in the call path, it is where the marshalling has to happen. JetBrains has a **Multiple Memory** proposal under consideration that would address this, and a @@ -173,12 +173,12 @@ committed date, and neither appears on the published roadmap, so neither can be | | `wasm32-emscripten` | `wasm32-wasi` | |---|---|---| | CPython support | Tier 3 from 3.14 (PEP 776) | **Tier 2** | -| bridge between the two modules | **JS exists** | none — needs Component Model, which Kotlin does not emit | +| bridge between the two modules | **JS exists** | none, needs Component Model, which Kotlin does not emit | | C extension modules | **dlopen works** | no dlopen in preview1; extensions must be linked in | | matches this project | **browser; the sample already targets `wasmJs`** | server/edge | The decisive row is the third. This project exists to share libraries, and on WASI a user could -not add a compiled package after the fact — the interpreter would have to be rebuilt. That +not add a compiled package after the fact, the interpreter would have to be rebuilt. That guts the goal on that platform, which outweighs WASI's better support tier. The second row is nearly as decisive: WASI has no JS host, so there is no mechanism at all to @@ -212,7 +212,7 @@ composed operations, the same ones measured on Android: int pmp_scratch(void); // reusable buffer address, once **Interning applies here and matters more.** A Kotlin string lives on the WasmGC heap and a C -string in linear memory, so copying costs a JS hop — but attribute and module names are +string in linear memory, so copying costs a JS hop, but attribute and module names are repeated literals, so caching the linear-memory address makes every call after the first cost nothing. This is the same fix measured on Android at 2238 ns → 148 ns, against a hop that is more expensive here than JNI's. @@ -227,7 +227,7 @@ Nothing above changes the layering: `wasmJsMain` supplies `actual`s for the same ## What survives if the toolchain improves -If Multiple Memory lands, the C shim stays — composition is wanted for its own sake — and only +If Multiple Memory lands, the C shim stays, composition is wanted for its own sake, and only the JS glue is replaced by direct memory access. The `actual` signatures do not change. That bounds the throwaway work to a few dozen lines of glue, which is the argument for this shape over waiting indefinitely. @@ -245,7 +245,7 @@ over waiting indefinitely. ## Start condition -Not now. `wasmJsMain`'s `actual`s cannot be written until it is settled what they implement — +Not now. `wasmJsMain`'s `actual`s cannot be written until it is settled what they implement, §1/§4 (GIL and automatic release) and §7 (upcall shape) are both in flight, and building against them now means building twice. @@ -253,9 +253,9 @@ them now means building twice. Two independent choices, easily conflated: -1. **How our C code is packaged** — statically linked into CPython, or a side module loaded by +1. **How our C code is packaged**: statically linked into CPython, or a side module loaded by `dlopen` at import time. -2. **Where CPython comes from** — built by us with Emscripten, or Pyodide's distribution. +2. **Where CPython comes from**: built by us with Emscripten, or Pyodide's distribution. ### Side module, on both counts of its own @@ -267,7 +267,7 @@ Two independent choices, easily conflated: | runtime | no indirection | dynamic-link indirection (small) | Having to rebuild the interpreter to change a binding is the same trap this project already hit -elsewhere — a `.def` edit needing a forced link and staging pass before it reached a device. An +elsewhere, a `.def` edit needing a forced link and staging pass before it reached a device. An extension module is what CPython already has a mechanism for; we are not doing anything unusual. ### Our own CPython, and no attempt at Pyodide compatibility @@ -286,7 +286,7 @@ The loss from building our own is narrower than it first appears: | | on our own build | |---|---| -| pure-Python wheels (`py3-none-any`) — no ABI tag | work unchanged | +| pure-Python wheels (`py3-none-any`), no ABI tag | work unchanged | | C extensions (numpy, pandas, lxml, cryptography) | must be built for our ABI | And the second row is true of Pyodide too, for anything outside the list they happen to have @@ -302,7 +302,7 @@ The section above is right about the decision and wrong about its cost. It was w `pyodide_2024_0_wasm32` tag and concluded that any wheel compatibility means being locked to a Pyodide release. That stopped being true. -[PEP 783 — Emscripten Packaging](https://peps.python.org/pep-0783/) is **Accepted**. It replaces +[PEP 783, Emscripten Packaging](https://peps.python.org/pep-0783/) is **Accepted**. It replaces the Pyodide-owned tag with a standard platform tag series that PyPI accepts: ``` @@ -314,8 +314,8 @@ Three things follow, and they change the trade-off rather than the conclusion. **The platform is versioned per Python feature release, not per distribution release.** The current series is `pyemscripten_2024_0` (3.12), `pyemscripten_2025_0` (3.13), `pyemscripten_2026_0` -(3.14). The earlier worry — "the web platform tracks Pyodide's cadence while every other platform -is pinned to 3.14" — does not apply: the platform version *is* the Python version. Pinning to 3.14 +(3.14). The earlier worry, "the web platform tracks Pyodide's cadence while every other platform +is pinned to 3.14", does not apply: the platform version *is* the Python version. Pinning to 3.14 from source and claiming `pyemscripten_2026_0` are the same act. The patch component exists as an escape hatch the PEP hopes never to use. @@ -351,7 +351,7 @@ no longer *owns* the tag. That is a real residual coupling, but it binds a flag | Pyodide as a dependency | the price of wheels | not required | | pure-Python wheels | work unchanged | work unchanged | -The decision stands — our own Emscripten CPython 3.14 — but the recorded limitation was +The decision stands, our own Emscripten CPython 3.14, but the recorded limitation was overstated. Compiled extensions are not lost by building our own; they are lost only if we build with flags that diverge from `pyemscripten_2026_0`. **Matching that flag set should be a build constraint from the start**, because retrofitting it means rebuilding the interpreter. @@ -365,8 +365,8 @@ backend, and [pydantic publishes Emscripten wheels to PyPI under it](https://pyd `pyodide.org/en/stable/development/abi.html` still returns 403 to automated fetching. The same documents are in the repository and `raw.githubusercontent.com` serves them, which is how the -following was read. Sources are `github.com/pyodide/pyodide` at tag **`314.0.4`** — -`docs/development/abi/314.md`, `docs/development/abi/flags.md`, `Makefile.envs` — and +following was read. Sources are `github.com/pyodide/pyodide` at tag **`314.0.4`**, +`docs/development/abi/314.md`, `docs/development/abi/flags.md`, `Makefile.envs`, and `github.com/pyodide/pyodide-build` `pyodide_build/config.py`. `Makefile.envs` at that tag pins the whole platform in four lines: @@ -382,16 +382,16 @@ PYODIDE_EMSCRIPTEN_VERSION ?= 5.0.3 |---|---|---| | Emscripten | **5.0.3** | 4.0.9 | | unwinding | `-fwasm-exceptions -sSUPPORT_LONGJMP=wasm`, at **compile and link** | same | -| `-sWASM_BIGINT` | required, but **default since Emscripten 4.0.0** — not typed explicitly | same | +| `-sWASM_BIGINT` | required, but **default since Emscripten 4.0.0**, not typed explicitly | same | | `-pthread` | prohibited; "if `-pthread` is used, the resulting libraries will not load" | same | | dependency lookup | full `RPATH` support, and `RPATH` is the *only* mechanism | same | | Rust | stable `1.93.0`; no nightly, no custom sysroot | nightly + custom sysroot | -| new static libs | `sqlite3` 3.39.0, `lzma`/xz 5.2.2 | — | +| new static libs | `sqlite3` 3.39.0, `lzma`/xz 5.2.2 | - | Statically linked into the main binary: `zlib`, `bzip2`, `sqlite3`, `lzma`, `zstd`, `libffi`, `libhiwire`, and the Emscripten JS libraries (`libGL`, `libegl.js`, `libwebgl.js`, `libhtml5*.js`, `libsdl.js`, `libwebsocket.js`, `libeventloop.js`, `liblz4`, the filesystem backends). **OpenSSL is -not** — Pyodide builds it as a shared side module (`libssl.so`/`libcrypto.so`, OpenSSL 1.1.1w) +not**, Pyodide builds it as a shared side module (`libssl.so`/`libcrypto.so`, OpenSSL 1.1.1w) found through `RPATH`. Extension modules are side modules. Pyodide documents `-sSIDE_MODULE=2` plus an explicit @@ -403,7 +403,7 @@ shim. defers normatively to Pyodide's documentation for the values. It does specify how the tag is derived, and that is the part that bites: `packaging` reads `sysconfig.get_config_var("PYEMSCRIPTEN_PLATFORM_VERSION")`. **That variable does not exist -anywhere in CPython 3.14.2** — `grep -r PYEMSCRIPTEN` over the whole checkout returns nothing. So a +anywhere in CPython 3.14.2**, `grep -r PYEMSCRIPTEN` over the whole checkout returns nothing. So a stock CPython Emscripten build cannot advertise the platform even if it matches the flags; the variable is something Pyodide's build injects. @@ -411,7 +411,7 @@ variable is something Pyodide's build injects. ## Correction: the JS layer is not mandatory -Everything above assumes Kotlin/Wasm can only reach CPython through JavaScript — that crossings +Everything above assumes Kotlin/Wasm can only reach CPython through JavaScript, that crossings cost a JS hop and that string data must be copied by JS between two isolated memories. Reading the Kotlin compiler shows both halves of that are wrong on the `wasmJs` target. This section records what was checked and where it stops. @@ -429,7 +429,7 @@ outright: So `pmp_getattr(i32, i32) -> i32` can be a direct wasm-to-wasm call. JS is still involved in *instantiating* the modules and wiring the import, but not in the call. This is exactly the -narrow integer interface the shape above already proposes — it just costs less than assumed. +narrow integer interface the shape above already proposes, it just costs less than assumed. The constraint is the one that makes it work: no type adapters means wasm primitive types only. No strings, no objects. Which is what the composed shim signature already looks like. @@ -441,16 +441,16 @@ No strings, no objects. Which is what the composed shim signature already looks | target | what the module does with its memory | |---|---| -| `wasmWasi` | **defines and exports** it — `WasmExport.Memory("memory", …)`, and the source comments that the name is a WASI ABI convention | +| `wasmWasi` | **defines and exports** it, `WasmExport.Memory("memory", …)`, and the source comments that the name is a WASI ABI convention | | `wasmJs` | **imports** it, as `WasmImportDescriptor("intrinsics", "memory")`, declared with 0 initial pages | That inverts the premise. A `wasmJs` module does not own a memory it defines; the host hands one in. And `kotlin.wasm.unsafe.Pointer` is an `i32` into precisely that memory. So the question "can Kotlin read CPython's `char*`" reduces to "can the host supply Emscripten's -memory as that import" — at the wasm level, both are just a `WebAssembly.Memory` in an import +memory as that import", at the wasm level, both are just a `WebAssembly.Memory` in an import object, and Emscripten exposes its own as `Module.wasmMemory`. **If that works, string -marshalling does not merely get cheaper, it disappears** — which matters more here than anywhere +marshalling does not merely get cheaper, it disappears**, which matters more here than anywhere else, because every measurement on Android and desktop found marshalling, not crossings, to be the dominant cost. @@ -465,8 +465,8 @@ the heap. This does not sink the idea, but it bounds it: -- **Reading and writing CPython-owned buffers** — addresses that CPython allocated and handed - back as `i32` — is safe. +- **Reading and writing CPython-owned buffers**: addresses that CPython allocated and handed + back as `i32`, is safe. - **Allocating from Kotlin inside the shared memory** is not, unless the allocator can be given a base address it does not currently accept. @@ -475,7 +475,7 @@ only from `allocate()`, has not been checked and decides whether the safe half i ### 4. The impossible direction stays impossible, and is already handled -None of this changes that a WasmGC reference cannot be stored in linear memory — that is a +None of this changes that a WasmGC reference cannot be stored in linear memory, that is a spec-level guarantee, not a maturity gap, so CPython can never hold a Kotlin object pointer in a `PyObject` field. The answer there remains an index into a Kotlin-side table, which is what `docs/design/object-lifetime.md` already specifies for Python→Kotlin. Kotlin/Wasm even blesses the @@ -486,13 +486,13 @@ mechanism: `JsReference` passes a Kotlin object as an opaque reference. | | as recorded above | after reading the compiler | |---|---|---| | Kotlin → CPython call | JS hop | **direct wasm call** (`@WasmImport`) | -| string marshalling | JS copies between two memories | **possibly none** — if the memory is shared | -| Kotlin allocating in that memory | assumed fine | **unsafe** — allocator starts at 0 | +| string marshalling | JS copies between two memories | **possibly none**, if the memory is shared | +| Kotlin allocating in that memory | assumed fine | **unsafe**, allocator starts at 0 | | CPython holding a Kotlin object | impossible | impossible (unchanged) | The composed-shim design does not change shape. What changes is its cost, and the reason to build it: composition was proposed here to amortise JS hops, and if the hops are gone it has to justify -itself on crossing count alone — the same argument that closed desktop composition in ROADMAP §6. +itself on crossing count alone, the same argument that closed desktop composition in ROADMAP §6. ### Unverified, in the order that decides the design @@ -524,7 +524,7 @@ released version**. Parsing the 2.4.10 output: the only length-prefixed `intrins So Kotlin does not import a memory today. It exports one. -**That inverts the plan, and the inverted plan is better** — because it needs nothing unreleased +**That inverts the plan, and the inverted plan is better**, because it needs nothing unreleased on either side. Emscripten has `-sIMPORTED_MEMORY`, which makes the CPython module import `env.memory` rather than define it. Instantiate Kotlin first, take `exports.memory`, grow it to Emscripten's initial size, hand it over as `env.memory`. Both halves are shipped features. @@ -541,7 +541,7 @@ pages after exception 0 pages after explicit allocate 2 (allocated at 0x0) ``` -The module declares `min_pages = 0`, and all 40 of its data segments are **passive** — they +The module declares `min_pages = 0`, and all 40 of its data segments are **passive**, they initialise WasmGC arrays through `array.new_data` and are never placed in linear memory. String interop was the obvious suspect for a hidden consumer and it is not one; Kotlin strings are WasmGC arrays and cross to JS as `externref`. @@ -557,7 +557,7 @@ memory this corrupts the heap on first use. The mitigation is not a workaround, it is the natural design: **CPython allocates, Kotlin only reads.** `Pointer(addr)` has a public constructor and its `loadInt`/`storeByte` members compile to -plain `i32.load`/`i32.store` with no allocator involvement — confirmed by compiling and running +plain `i32.load`/`i32.store` with no allocator involvement, confirmed by compiling and running it. Every address we would ever dereference comes back from a C call. `withScopedMemoryAllocator` must therefore never appear in `wasmJsMain`. That belongs in the @@ -571,11 +571,11 @@ source-set README as a hard rule, in the same category as desktop's "always `inv | who owns the shared memory | CPython, imported by Kotlin (master-only) | **Kotlin exports it, Emscripten imports it** | | Kotlin reading `char*` | impossible | `Pointer(addr).loadByte()`, no JS, no allocator | | string marshalling | dominant cost | **none, if the memory is shared** | -| CPython holding a Kotlin object | impossible | impossible (unchanged — handle index) | +| CPython holding a Kotlin object | impossible | impossible (unchanged, handle index) | If this holds end to end, WASM stops being the outlier and lands close to the iOS shape: no boundary for data, a direct call for control, and a handle table for the one direction WasmGC -forbids. The composition argument weakens accordingly — it was proposed here to amortise JS hops, +forbids. The composition argument weakens accordingly, it was proposed here to amortise JS hops, and without them it must justify itself on crossing count alone, which is what closed desktop composition in ROADMAP §6. @@ -597,15 +597,15 @@ composition in ROADMAP §6. The section above listed three unverified items and said the first needed `emsdk`. It is installed, both experiments are built and run, and both pass. `wasm-experiment/` reproduces them. -### Test A — `@WasmImport` binds to an Emscripten export +### Test A, `@WasmImport` binds to an Emscripten export Kotlin called `add_two` compiled by `emcc` and got 42. The compiler emits a real wasm import, `(import "./probeA-wrapper.mjs" "add_two" (func …))`, and wires the ES module into the import -object. The value supplied prints as `function 2() { [native code] }` — a raw wasm export, not a -JS wrapper — so the call is wasm-to-wasm with no JS frame, which is what the compiler's own +object. The value supplied prints as `function 2() { [native code] }`, a raw wasm export, not a +JS wrapper, so the call is wasm-to-wasm with no JS frame, which is what the compiler's own `callingWasmDirectly.kt` test intends. -### Test B — Emscripten imports the memory Kotlin exports +### Test B, Emscripten imports the memory Kotlin exports ``` C: get_static_message() -> 0x400 @@ -626,14 +626,14 @@ Emscripten refused Kotlin's memory at first: LinkError: memory import has no maximum limit, expected at most 4294967295 ``` -Kotlin emits `WasmLimits(0, null)` — no maximum — and wasm requires a supplied memory's limits to +Kotlin emits `WasmLimits(0, null)`, no maximum, and wasm requires a supplied memory's limits to sit inside the importer's, so an unbounded memory can never satisfy any Emscripten import. Patching section 5 to `{min: 0, max: 32768}` made everything link and both directions work. Nothing else about the pairing needed changing. **That is a compiler-chosen constant, not a design problem.** It is worth a YouTrack issue; a `-Xwasm-memory-maximum`-style knob, or simply emitting the wasm32 ceiling, would remove the need -to post-process the binary. Until then a build step can patch it, which is ugly but bounded — the +to post-process the binary. Until then a build step can patch it, which is ugly but bounded, the memory section is six bytes and nothing in the format holds an absolute offset. ### The constraint that shapes the design @@ -647,12 +647,12 @@ Emscripten -sIMPORTED_MEMORY needs Kotlin's memory before that Wasm imports are supplied up front, so that is a cycle. Three ways out: -1. **Kotlin imports the memory instead of exporting it.** No cycle — Emscripten instantiates +1. **Kotlin imports the memory instead of exporting it.** No cycle, Emscripten instantiates first, then Kotlin receives both the memory and the functions. This is `importWasmMemoryInsteadOfExport = isWasmJsTarget`, present at master and in no release. It is the configuration this design wants, and the second reason to talk to JetBrains. 2. **JS trampolines for calls, shared memory for data.** Available today. Costs a JS hop per - call and keeps the data path free — and the data path is the half that dominated every + call and keeps the data path free, and the data path is the half that dominated every measurement on Android and desktop, so this captures most of the win. 3. Copy through JS, as originally designed. @@ -661,10 +661,10 @@ Wasm imports are supplied up front, so that is a cycle. Three ways out: | | original design | proven | |---|---|---| | control path | JS hop per call | direct wasm call, **or** JS trampoline if sharing memory | -| data path | JS copies between two memories | **none — one memory** | +| data path | JS copies between two memories | **none, one memory** | | string marshalling | dominant cost, needs interning | **absent** | -| composition | needed to amortise JS hops | **weakly motivated** — same argument that closed §6 | -| CPython holding a Kotlin object | impossible | impossible — handle index, per `object-lifetime.md` | +| composition | needed to amortise JS hops | **weakly motivated**, same argument that closed §6 | +| CPython holding a Kotlin object | impossible | impossible, handle index, per `object-lifetime.md` | WASM stops being the outlier. With option 2 it is roughly Android's shape with a cheaper data path; with option 1 it is close to iOS. @@ -693,7 +693,7 @@ path; with option 1 it is close to iOS. The result above was nearly reported wrong. `runPromise` resolves after `main()` returns, so the coroutine's page count was taken *after* the `withScopedMemoryAllocator` demo had already grown the memory to 2 pages. The probe printed `pages inside coroutine 2` and that number said nothing -about coroutines — it was the allocator's growth, observed late. +about coroutines, it was the allocator's growth, observed late. Gating the allocator demo off and re-running gives the reading that means something: @@ -742,7 +742,7 @@ intrinsics: { ``` Replacing that one expression with Emscripten's `Module.wasmMemory` is the whole integration. No -binary patching, no compiler flag, no `max` limit to negotiate — `patch-memory-max.py` becomes +binary patching, no compiler flag, no `max` limit to negotiate, `patch-memory-max.py` becomes history rather than a build step. ### It also removes the constraint that shaped the design @@ -754,7 +754,7 @@ trampolines for the calls, measured at 2.73x a direct call. With Kotlin importing, the order resolves: instantiate Emscripten first, then hand Kotlin both the memory and the functions. **Direct wasm-to-wasm calls and a shared linear memory at the same -time** — which is the configuration this design wanted and had written off as unavailable. +time**, which is the configuration this design wanted and had written off as unavailable. ### What that leaves @@ -764,8 +764,8 @@ cost), build CPython for `pyemscripten_2026_0`, write the composed shim, and sup `Module.wasmMemory` in place of the placeholder above. Two facts from earlier still hold and still constrain the shape. Kotlin never touches its linear -memory on its own — measured across strings, collections, exceptions, a 10 MiB `ByteArray`, a -100k-object graph, the `ArrayBuffer` bridge and coroutines — so handing it to Emscripten costs +memory on its own, measured across strings, collections, exceptions, a 10 MiB `ByteArray`, a +100k-object graph, the `ArrayBuffer` bridge and coroutines, so handing it to Emscripten costs nothing. And `withScopedMemoryAllocator` must never appear in `wasmJsMain`, because it allocates from address 0 upward, on top of Emscripten's static data. @@ -790,7 +790,7 @@ non-function exports none ``` `max=none` on the *import* is what makes this direction work. A wasm import must be satisfied by -a memory whose limits sit inside the declared ones, so an unbounded import accepts anything — +a memory whose limits sit inside the declared ones, so an unbounded import accepts anything, including Emscripten's bounded memory. The old direction was unsatisfiable for exactly the mirror-image reason, and `patch-memory-max.py` existed to force it. It is no longer used. @@ -805,7 +805,7 @@ intrinsics: { memory: new WebAssembly.Memory({ initial: 0 }), tag: wasmTag } Ordering comes free from ES modules. `wasm-experiment.import-object.mjs` imports the wrapper, the wrapper has a top-level `await factory()`, so Emscripten is fully instantiated before Kotlin's -import object is built. Emscripten is compiled exactly as it would be alone — no +import object is built. Emscripten is compiled exactly as it would be alone, no `-sIMPORTED_MEMORY`, no binary patching. Kotlin is the side that adapts. ### Both halves hold at once @@ -820,8 +820,8 @@ Kotlin: writeCStringAt(malloc'd addr, "written-by-kotlin") C: str_len -> 17, UTF8ToString -> "written-by-kotlin" PASS ``` -The value handed to the import still prints as `function 2() { [native code] }` — a raw wasm -export, no JS wrapper — so the control path is wasm-to-wasm with the memory shared. +The value handed to the import still prints as `function 2() { [native code] }`, a raw wasm +export, no JS wrapper, so the control path is wasm-to-wasm with the memory shared. ### And it survives the memory growing @@ -849,7 +849,7 @@ C: malloc(64 MiB) -> 0x10638, 258 pages -> 1026 pages So removing the cycle is worth about 9–12 ns per crossing. That is the whole benefit of option 1 over option 2, and it is now available. -### Measured: the data path — and the earlier conclusion was wrong +### Measured: the data path, and the earlier conclusion was wrong Everything above said string marshalling "does not merely get cheaper, it disappears". **It does not.** Both loops below run in Kotlin and end with a Kotlin `String` built from the same C @@ -870,7 +870,7 @@ Two things account for it, and neither is the boundary: `builtins: ['js-string']`. `UTF8ToString` produces a JS string, and handing that to Kotlin is not a conversion. The "copy through JS" path was assumed to pay for a copy that no longer exists. -2. **`ByteArray.decodeToString()` is pathological on long input** — 53 µs of the 58 µs figure at +2. **`ByteArray.decodeToString()` is pathological on long input**: 53 µs of the 58 µs figure at 4000 bytes, about 13 ns per byte, against 0.2 ns per byte at 27 bytes. `concatToString()` on a `CharArray` costs a tenth of it. Worth a separate report upstream. @@ -894,7 +894,7 @@ The honest rule that comes out of this is not "marshalling disappears" but: So the composition arithmetic is now fully determined. Walking a list without a shim means N direct `PyList_GetItem` calls at **5.0 ns** of crossing each. With a shim it is one crossing plus -N shared reads at **0.7 ns**. **Composition buys ~4.3 ns per element and nothing else** — the +N shared reads at **0.7 ns**. **Composition buys ~4.3 ns per element and nothing else**, the CPython-side work is identical either way. ROADMAP §6 closed desktop composition at a 10% saving. Test D below measures the same question @@ -905,7 +905,7 @@ against the real interpreter and answers it more sharply. ## Test D: it works against real CPython, and it closes the composition question Tests A and C run against a 4 KB toy compiled by `emcc`. Test D runs against **CPython 3.14.2 -built for `wasm32-emscripten`** — 10 MB of wasm, 8191 exports, `-sMAIN_MODULE`, a growable memory, +built for `wasm32-emscripten`**, 10 MB of wasm, 8191 exports, `-sMAIN_MODULE`, a growable memory, instantiated by Emscripten's own glue. The build is `Tools/wasm/emscripten` (PEP 776, Tier 3 from 3.14) under Emscripten 5.0.3; see the build notes below. @@ -929,7 +929,7 @@ survives the interpreter growing its own memory by 600 pages underneath. ### The one build change CPython needed, and it is not ABI-sensitive -The Stable ABI symbols are all there — `-sMAIN_MODULE` is `LINKABLE`, so Emscripten exports +The Stable ABI symbols are all there, `-sMAIN_MODULE` is `LINKABLE`, so Emscripten exports everything and emits `EXPORTED_FUNCTIONS is not valid with LINKABLE set` for CPython's own list. **This retires the recorded worry that "CPython must be built with an explicit `EXPORTED_FUNCTIONS` list; a stock Pyodide build strips the C API by dead-code elimination."** The @@ -944,7 +944,7 @@ interesting ones, so there is nothing to hand to `@WasmImport`. One flag fixes i ``` After a relink, `Module.wasmExports.PyRun_SimpleString` prints as `function 4426() { [native -code] }` — a raw wasm export — and `Module.wasmMemory` is the `WebAssembly.Memory` Kotlin's +code] }`, a raw wasm export, and `Module.wasmMemory` is the `WebAssembly.Memory` Kotlin's `intrinsics.memory` needs. Both are JS-glue settings and neither appears in PEP 783's ABI-sensitive list, so this costs nothing against the platform tag. @@ -956,29 +956,29 @@ Each row adds a pure-Kotlin optimisation: | | ns | | |---|---|---| | naive | 259.5 | 2 `malloc` + 2 `free` + 2 string writes + 4 API calls | -| + interned C strings | 185.4 | names allocated once — the Android fix, for free here | -| + module/dict hoisted | **65.2** | 2 API calls — the floor | +| + interned C strings | 185.4 | names allocated once, the Android fix, for free here | +| + module/dict hoisted | **65.2** | 2 API calls, the floor | | one crossing (`PyErr_Occurred`) | **2.9** | direct `@WasmImport`, real interpreter | **75% of the naive cost is removable without shipping a single line of C.** What is left is 65 ns -of two CPython calls, of which the crossings are 5.8 ns — 9% of the floor, 2% of the naive figure. +of two CPython calls, of which the crossings are 5.8 ns, 9% of the floor, 2% of the naive figure. A composed `pmp_getattr` could merge those two calls into one and save **2.9 ns**. The bulk case ends the same way. Composition there buys the difference between a crossing and a shared-memory read, 2.9 ns against 0.7 ns per element, and CPython does identical work either way. -The constraint that kept the case alive — `PySequence_Fast_ITEMS` is a macro over +The constraint that kept the case alive, `PySequence_Fast_ITEMS` is a macro over `PyListObject->ob_item` and `abi3t` makes `PyObject` incomplete, so shared memory does *not* let -Kotlin walk a list's storage — turns out not to matter, because N calls at 2.9 ns is cheap. +Kotlin walk a list's storage, turns out not to matter, because N calls at 2.9 ns is cheap. **Verdict: no composed shim on WASM.** Same answer as ROADMAP §6 gave desktop, reached the same way, and with a wider margin: desktop's composition was worth 10%, this is worth 1–2% after the -free wins are taken. The signatures sketched earlier — `pmp_getattr`, `pmp_exec`, `pmp_list_items`, -`pmp_str_utf8`, `pmp_decref`, `pmp_scratch` — were motivated by amortising JS hops, and the hops +free wins are taken. The signatures sketched earlier, `pmp_getattr`, `pmp_exec`, `pmp_list_items`, +`pmp_str_utf8`, `pmp_decref`, `pmp_scratch`, were motivated by amortising JS hops, and the hops are gone. What replaces them is ordinary `wasmJsMain` Kotlin: `@WasmImport` per Stable ABI function, an interning cache for repeated names, and `Pointer` reads for everything else. That also removes the second build pipeline. No extension module to build, version and ship -against a specific interpreter — which was the largest recurring cost in the plan. +against a specific interpreter, which was the largest recurring cost in the plan. ### What is still not answered @@ -1007,15 +1007,15 @@ Read off the actual command lines, not the documentation: | | stock CPython 3.14.2 | `pyemscripten_2026_0` | |---|---|---| -| Emscripten | 5.0.3 (we pinned it) | **5.0.3** — matches | -| `-sWASM_BIGINT` | passed explicitly in `LDFLAGS_NODIST` | required; default since Emscripten 4.0 — matches | -| `-pthread` | absent; `configure` even reports `emcc accepts -pthread... no` | prohibited — matches | -| `-fPIC` | yes (required by `MAIN_MODULE`) | yes — matches | -| dynamic linking | `--enable-wasm-dynamic-linking` → `-sMAIN_MODULE`; extensions built `-shared -sSIDE_MODULE=1` | `MAIN_MODULE=1` / side modules — matches | -| **unwinding** | **nothing** — no `-fwasm-exceptions`, no `-sSUPPORT_LONGJMP=wasm` | **required at compile and link** — **DIVERGES** | -| **static libs** | zlib, bzip2, sqlite3, libffi, mpdecimal; **no lzma, no zstd** | + lzma 5.2.2, zstd — **DIVERGES** | -| **OpenSSL** | **absent** — "Could not build the ssl module" | shared side module, OpenSSL 1.1.1w — **DIVERGES** | -| platform variable | `PYEMSCRIPTEN_PLATFORM_VERSION` is **not defined anywhere in CPython 3.14.2** | `packaging` reads it to emit the tag — **DIVERGES** | +| Emscripten | 5.0.3 (we pinned it) | **5.0.3**, matches | +| `-sWASM_BIGINT` | passed explicitly in `LDFLAGS_NODIST` | required; default since Emscripten 4.0, matches | +| `-pthread` | absent; `configure` even reports `emcc accepts -pthread... no` | prohibited, matches | +| `-fPIC` | yes (required by `MAIN_MODULE`) | yes, matches | +| dynamic linking | `--enable-wasm-dynamic-linking` → `-sMAIN_MODULE`; extensions built `-shared -sSIDE_MODULE=1` | `MAIN_MODULE=1` / side modules, matches | +| **unwinding** | **nothing**, no `-fwasm-exceptions`, no `-sSUPPORT_LONGJMP=wasm` | **required at compile and link**, **DIVERGES** | +| **static libs** | zlib, bzip2, sqlite3, libffi, mpdecimal; **no lzma, no zstd** | + lzma 5.2.2, zstd, **DIVERGES** | +| **OpenSSL** | **absent**, "Could not build the ssl module" | shared side module, OpenSSL 1.1.1w, **DIVERGES** | +| platform variable | `PYEMSCRIPTEN_PLATFORM_VERSION` is **not defined anywhere in CPython 3.14.2** | `packaging` reads it to emit the tag, **DIVERGES** | So the stock Tier 3 build is close but cannot claim the tag, and the gap is real work rather than a flag flip: the unwinding ABI has to be added at both compile and link, `lzma`/`zstd`/OpenSSL have to @@ -1023,7 +1023,7 @@ be built as ports or side modules, and something has to define `PYEMSCRIPTEN_PLA A practical trap for whoever does it: the driver hardcodes `CFLAGS=-DPY_CALL_TRAMPOLINE -sUSE_BZIP2` in its `configure` argv and appends user arguments *after*, and autoconf takes the -last assignment — so passing `CFLAGS=…` to add `-fwasm-exceptions` silently drops +last assignment, so passing `CFLAGS=…` to add `-fwasm-exceptions` silently drops `-DPY_CALL_TRAMPOLINE`. The full string has to be repeated. **Not verified:** that the ABI-matching build succeeds, or that a PyPI `pyemscripten_2026_0` wheel @@ -1031,13 +1031,13 @@ loads into it. Only the stock build was run. ### Environment note -Installing Emscripten 5.0.3 through `emsdk install` **replaces `~/emsdk/upstream` in place** — the +Installing Emscripten 5.0.3 through `emsdk install` **replaces `~/emsdk/upstream` in place**, the previously active 6.0.6 now reports as not installed and would have to be re-downloaded. The active version is a global, shared setting, not per-project. --- -## Proven: the tag claim is real — a PyPI `pyemscripten_2026_0` wheel loads +## Proven: the tag claim is real, a PyPI `pyemscripten_2026_0` wheel loads The section above listed four divergences between the stock Tier 3 build and `pyemscripten_2026_0` and marked the whole thing "not verified". It has now been built and @@ -1063,8 +1063,8 @@ linked. | | matched? | did it gate the wheel? | |---|---|---| -| unwinding ABI | **yes, this build** | **yes — this was the whole gate** | -| `PYEMSCRIPTEN_PLATFORM_VERSION` | **yes, via a one-line CPython patch** | no — gates *claiming* the tag, not loading | +| unwinding ABI | **yes, this build** | **yes, this was the whole gate** | +| `PYEMSCRIPTEN_PLATFORM_VERSION` | **yes, via a one-line CPython patch** | no, gates *claiming* the tag, not loading | | lzma, zstd | still missing | no | | OpenSSL | still missing | no | @@ -1092,7 +1092,7 @@ cpython314/python.wasm no tag section, exports neither ``` So `-fwasm-exceptions -sSUPPORT_LONGJMP=wasm` is not a nice-to-have that affects code -generation quality — it is a link-level contract, and it is checked. `lzma`/`zstd`/OpenSSL are +generation quality, it is a link-level contract, and it is checked. `lzma`/`zstd`/OpenSSL are ABI-sensitive in the Pyodide flag list but only bind wheels that link *them*; they cost the stdlib modules `_lzma`, `_zstd`, `_hashlib`, `_ssl` and nothing else here. @@ -1115,7 +1115,7 @@ def _emscripten_platforms(): yield f"pyemscripten_{v}_wasm32" ``` -Before defining it, the interpreter loads the wheel but cannot claim the tag — its platform tag +Before defining it, the interpreter loads the wheel but cannot claim the tag, its platform tag is `emscripten_5_0_3_wasm32` and no `pyemscripten_*` tag is produced at all. The obvious way to define it is a Makefile variable, because that is what `sysconfig @@ -1127,10 +1127,10 @@ int('2026_0') == 20260 int('2025_0') == 20250 int('2024_0') == 20240 ``` so the config var comes out as the integer `20260` and `packaging` emits -`pyemscripten_20260_wasm32` — a plausible-looking tag that matches nothing. Every version in +`pyemscripten_20260_wasm32`, a plausible-looking tag that matches nothing. Every version in the PEP's series has this shape, so it is not specific to 2026. -`sysconfig` already has the opt-out — `_ALWAYS_STR`, which holds the two Apple deployment +`sysconfig` already has the opt-out, `_ALWAYS_STR`, which holds the two Apple deployment targets for the same reason. Adding one name to it is the whole fix: ```python @@ -1154,7 +1154,7 @@ mechanism a packager would naturally reach for corrupts it without any error. ### What this changes for the plan -The earlier decision — our own Emscripten CPython 3.14, no Pyodide dependency — stands, and the +The earlier decision, our own Emscripten CPython 3.14, no Pyodide dependency, stands, and the recorded limitation shrinks again. "Compiled third-party extensions are a known limitation on this platform" is now false for anything published under the PEP 783 tag. The remaining limitation is narrower and ordinary: packages that need `lzma`, `zstd` or OpenSSL need those @@ -1164,8 +1164,8 @@ built, and packages not published for the tag still have to be built. ## Upcalls: measured, and better than every prediction in this document -§5 and §7 step 3 specify one mechanism — `@JsExport` the trampoline, wrap it in a JS closure, -hand it to `addFunction` — and "Unresolved" writes it off as "goes through JS, unmeasured, and +§5 and §7 step 3 specify one mechanism, `@JsExport` the trampoline, wrap it in a JS closure, +hand it to `addFunction`, and "Unresolved" writes it off as "goes through JS, unmeasured, and likely worse than every other platform". Tests E and F measure it, and measure an alternative this document does not consider. @@ -1180,8 +1180,8 @@ String(raw.kotlin_upcall_add) -> function 5097() { [native code] } `WebAssembly.Table.prototype.set` accepts any exported wasm function whatever instance produced it, so it can go straight into Emscripten's `__indirect_function_table`, and Emscripten's -`call_indirect` reaches it directly. JS appears **once, at registration** — to put a funcref in -a table — and never in a call. +`call_indirect` reaches it directly. JS appears **once, at registration**, to put a funcref in +a table, and never in a call. ``` table.grow(1) -> 2; table.set(2, kotlin_upcall_add) accepted @@ -1192,16 +1192,16 @@ Test E, 10,000,000 indirect calls issued from inside C so no JS is in the timed | | ns/call | crossing | |---|---|---| -| C → C (control, same module) | 0.7 | — | +| C → C (control, same module) | 0.7 | - | | **C → Kotlin, `table.set`** | **3.1** | **2.4 ns** | | C → Kotlin, `addFunction` + JS closure (the design's mechanism) | 10.9 | 10.2 ns | The design's mechanism is **3.5x** the direct one. And the direct upcall at ~3 ns is the mirror -of the direct downcall at ~5 ns — the two directions cost the same order, which is the thing +of the direct downcall at ~5 ns, the two directions cost the same order, which is the thing this document assumed could not be true. `addFunction` handed the wasm export directly works too, and deduplicates onto the entry -`table.set` already installed — so it is the same path, not a third one. +`table.set` already installed, so it is the same path, not a third one. ### Re-entrancy holds: the WasmGC heap is live inside a frame CPython pushed @@ -1244,7 +1244,7 @@ type(kotlin_cb).__name__ = "builtin_function_or_method" Python sees an ordinary builtin. Variadic arguments arrive intact, 1000 calls in a Python loop all return correctly, and a bad argument raises rather than corrupting anything. The return -value is built by Kotlin calling `PyLong_FromLong` — **a downcall issued from inside an +value is built by Kotlin calling `PyLong_FromLong`, **a downcall issued from inside an upcall**, so both directions are live on one stack. ### But CPython's own wasm trampoline never installs, and that is an upstream defect @@ -1269,11 +1269,11 @@ It was called. **The JS fallback is live**, and instrumenting the swallowed `cat ``` The `EM_JS` initialiser runs while `python.mjs` is still evaluating, and needs `wasmTable` and -`wasmMemory` — which in this configuration are **exports** of `python.wasm`, assigned only after +`wasmMemory`, which in this configuration are **exports** of `python.wasm`, assigned only after instantiation. The `LinkError` is swallowed by a bare `catch (e) {}` and the JS fallback is kept silently. -This is an instantiation cycle, not an ordering slip — the same shape as the one Kotlin's memory +This is an instantiation cycle, not an ordering slip, the same shape as the one Kotlin's memory import dissolved, pointing the other way. The trampoline module imports `env.memory` and `env.__indirect_function_table` from the very module whose import object needs the trampoline. `-sIMPORTED_MEMORY` was tried: it fixes the memory half and the failure moves to the table, @@ -1288,22 +1288,22 @@ and Emscripten has no imported-table setting to finish the job. So on this toolc wasm trampoline cannot install at all. **The consequence is symmetric, which is why it does not change the design.** Every -`PyCFunction` call in the build crosses a JS frame — a C extension's exactly as much as +`PyCFunction` call in the build crosses a JS frame, a C extension's exactly as much as Kotlin's. Measured like for like, `abs()` (a C builtin declared `METH_O`) against a Kotlin `METH_O` function with the identical wasm signature: | | ns/call | |---|---| | pure Python `def py_cb(x): return x+1` | 59 | -| C builtin `abs(x)` — `METH_O` | 73 | -| **Kotlin `kotlin_cb_o(x)` — `METH_O`** | **87** | +| C builtin `abs(x)`, `METH_O` | 73 | +| **Kotlin `kotlin_cb_o(x)`, `METH_O`** | **87** | | | **14 ns attributable to the crossing** | | Kotlin, `METH_VARARGS` | 161 | | Kotlin, `METH_VARARGS` + tuple read | 183 | The `METH_VARARGS` rows carry CPython's argument-tuple construction; that cost is identical for a C extension and is not the crossing. 14 ns against `abs` matches Test E's `addFunction` -figure, as it should — both are the JS-frame path. +figure, as it should, both are the JS-frame path. ### What this means for §7 @@ -1312,8 +1312,8 @@ figure, as it should — both are the JS-frame path. | iOS | a plain function call | | Android | a JNI callback | | desktop | a Panama upcall stub | -| **WASM, slot reached by `call_indirect`** | **a plain `call_indirect` — ~3 ns** | -| **WASM, `PyMethodDef` today** | the same JS trampoline every C extension pays — ~14 ns over C | +| **WASM, slot reached by `call_indirect`** | **a plain `call_indirect`, ~3 ns** | +| **WASM, `PyMethodDef` today** | the same JS trampoline every C extension pays, ~14 ns over C | **§7's design holds on WASM, and the caveat it carries is wrong.** "WASM will have permanently higher per-call overhead than Native/JVM due to the WasmGC ↔ JS ↔ Emscripten boundary" describes @@ -1342,11 +1342,11 @@ Nothing structural. `wasmJsMain` can be written against §7 as amended. What is toolchain one. - ~~Whether Kotlin/Wasm offers a GC hook equivalent to `Cleaner`/`createCleaner`.~~ **Answered.** The stdlib does not, but the *host* does, and `toJsReference()` reaches it without pinning the - object it hands over — measured at `alive 0 / 200` with JS holding only a `WeakRef` and a + object it hands over, measured at `alive 0 / 200` with JS holding only a `WeakRef` and a `FinalizationRegistry` entry. `registerCleaner` is built on that. The second finding cost more: a collection is observable only across a **macrotask** boundary, because a `WeakRef` outlives the job that created it and the registry callback is delivered as a task, and - `FinalizationRegistry.prototype.cleanupSome()` — which would have made it synchronous — has been + `FinalizationRegistry.prototype.cleanupSome()`, which would have made it synchronous, has been removed from V8. See `src/wasmJsMain/README.md` and ROADMAP §10 for the numbers. diff --git a/docs/design/architecture.md b/docs/design/architecture.md index db08d8c25..a2bbb3ca4 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -31,7 +31,7 @@ cinterop 어느 쪽과도 겹치지 않는다), 바로 아래에 적힌 `expect 크래시가 난다.** 마이그레이션 대상 함수에서 `inline` 을 떼면 해결된다. `EmbedAPI.kt` 의 `inline` 중 다수는 컴파일러가 이미 "인라인 이득 없음" 경고를 내고 있어, 떼는 데 실질적 손해가 없다. (크래시는 Kotlin 2.0.20 에서 관찰됐다. 빌드는 이후 Kotlin 2.4.20-Beta2 로 올라갔고(`gradle/libs.versions.toml`), -이 크래시를 그 버전에서 다시 확인하지는 않았다 — 그래서 AGENTS.md §16 은 여전히 JVM 계열 통합 전에 +이 크래시를 그 버전에서 다시 확인하지는 않았다, 그래서 AGENTS.md §16 은 여전히 JVM 계열 통합 전에 확인하라고 한다.) ## FFI 계층 @@ -58,7 +58,7 @@ desktop·native 와 동일하다. ### CPython C 매크로는 바인딩할 수 없다 `PyLong_Check` 계열은 헤더의 `#define` 이라 심볼로 export 되지 않는다. `nm` 으로 확인했다. 그래서 -`ffi/PyTypeChecks.kt` 가 common Kotlin 으로 11종을 구현한다 — `PyObject_IsInstance` 와 캐시된 타입 +`ffi/PyTypeChecks.kt` 가 common Kotlin 으로 11종을 구현한다, `PyObject_IsInstance` 와 캐시된 타입 객체를 쓰며, `expect`/`actual` 분기가 없으므로 네 플랫폼 동작이 구조적으로 동일하다. ## 타입 래퍼 계층 @@ -100,8 +100,8 @@ Kotlin 제약 때문에 다이어그램대로 갈 수 없던 지점들: ## 검증 수단 명령 목록과 어떤 소스셋을 덮는지는 AGENTS.md §15 가 기준이다 (`compileKotlinAndroidNativeArm64`, -`compileKotlinIosSimulatorArm64`, `compileKotlinDesktop`, 그리고 `ksp-fixtures` 세 모듈을 각각 따로). -테스트는 컴파일 검증만이 아니다 — 데스크톱(`desktopTest`), iOS 시뮬레이터, 안드로이드 기기, wasm(Node) +`compileKotlinIosSimulatorArm64`, `compileKotlinDesktop`, 그리고 `fixtures/` 아래 세 모듈을 각각 따로). +테스트는 컴파일 검증만이 아니다, 데스크톱(`desktopTest`), iOS 시뮬레이터, 안드로이드 기기, wasm(Node) 에서 실제 CPython 을 구동하는 스위트가 있다 (SPEC §0 의 "Test path" 열). `Python.framework` 에는 표준 라이브러리가 없어 `Py_Initialize()` 가 @@ -128,7 +128,7 @@ libpython3.14 **The object model must never reference `bindings`.** `PyObject` and friends call the EmbedAPI functions in `python.native.ffi` and nothing below them. `bindings` is an implementation -detail that only `EmbedAPI..kt` is allowed to know about — it does not even exist on +detail that only `EmbedAPI..kt` is allowed to know about, it does not even exist on the native targets, which reach CPython through cinterop instead. Concretely, this is correct: @@ -149,8 +149,8 @@ fun getAttr(name: String) = PyObject(bindings.asmGetAttr(pointer.raw(), name)) ### Consequence for composed operations -A composed call — one that does a whole binder operation natively and crosses the boundary -once — is still a platform implementation difference, so it belongs in the FFI layer, not in +A composed call, one that does a whole binder operation natively and crosses the boundary +once, is still a platform implementation difference, so it belongs in the FFI layer, not in the object model: ```kotlin @@ -206,7 +206,7 @@ expect abstract class PyAutoCloseable(pointer: NativePointer) { the base held a hardcoded (and commented-out) `//PyDecRef(this.pointer)`; after, it calls `clean()` and `PyObject` supplies it. -**3. Split the single `jvmMain` implementation into per-platform leaves** — and this fixed a +**3. Split the single `jvmMain` implementation into per-platform leaves**, and this fixed a latent crash. `jvmMain`'s version called `Cleaner.create()` unconditionally, and Android inherited it while minSdk was 24. `java.lang.ref.Cleaner` does not exist below API 33, so every `PyObject` construction on API 24-32 would have thrown `NoClassDefFoundError`. The new @@ -215,7 +215,7 @@ every `PyObject` construction on API 24-32 would have thrown `NoClassDefFoundErr **4. As a consequence, `PyObject` stopped being `expect`/`actual`.** Once the base class was expressible in `commonMain`, `PyObject` could inherit it there and finally have real method -bodies — which is how the commented logic became code. +bodies, which is how the commented logic became code. | platform | lifetime mechanism after the split | |---|---| @@ -229,17 +229,17 @@ bodies — which is how the commented logic became code. The split is sound and stays. What was lost was incidental: `PyObject.android.kt` had been the only place an `actual external fun` could go, and that was the hook for routing an operation -through a single composed JNI call. Measurement later put a number on it — 5.5x on one +through a single composed JNI call. Measurement later put a number on it, 5.5x on one `getAttr`, 11x on a 1000-element list conversion. Recovering it does not mean reverting the commit; it means expressing composed operations in the FFI layer, as above. (The `PyObject_GetAttrComposed` snippet is illustrative, not a function in the tree. For string-carrying calls, interning later beat -composition and replaced it — `marshalling-design.md`; composition remains only where crossing count +composition and replaced it, `marshalling-design.md`; composition remains only where crossing count scales with N.) ## Test layering: low-level and assembled Tests are split by which layer they exercise, and both layers must be covered on every -platform. This is not organisational tidiness — the gap between them hid a defect for the whole +platform. This is not organisational tidiness, the gap between them hid a defect for the whole of the object model's life. **Low-level** tests call EmbedAPI functions one at a time: does this function return what its C @@ -254,7 +254,7 @@ Android, everything under `androidInstrumentedTest` that reaches through `bindin ### Why both, everywhere At the time, `commonTest` was entirely assembled and ran only on iOS and desktop. Android's -instrumented tests were entirely low-level (today `commonTest` also runs on Android and wasm; SPEC §0). So no test anywhere called the real object model on a device — +instrumented tests were entirely low-level (today `commonTest` also runs on Android and wasm; SPEC §0). So no test anywhere called the real object model on a device, and it turned out `Python3.exec` crashes the process on its first call there, because several functions it reaches still take a Kotlin `String` straight across JNI. @@ -276,5 +276,5 @@ afterwards. Go through `PythonTestFixture`, which initialises once per process. **Read test counts from a cleaned results directory.** Gradle leaves XML from previous runs in place, so a crashed run can report the previous run's numbers. A suite that "passes 111 tests" -while crashing is what stale XML looks like — delete `build/test-results//` when a +while crashing is what stale XML looks like, delete `build/test-results//` when a count needs to be trusted. diff --git a/docs/design/binding-policy.md b/docs/design/binding-policy.md index 9603149ec..299aa210e 100644 --- a/docs/design/binding-policy.md +++ b/docs/design/binding-policy.md @@ -1,4 +1,4 @@ -# 노출 정책 — 무엇이 Python 에서 보이는가 +# 노출 정책, 무엇이 Python 에서 보이는가 Kotlin 의 무엇을 함수 테이블에 등록해 Python 에 노출할지에 대한 규칙. 테이블 자체의 구조는 [`upcall.md`](upcall.md). @@ -6,7 +6,7 @@ Kotlin 의 무엇을 함수 테이블에 등록해 Python 에 노출할지에 이 문서의 표는 **KSP 프로세서가 소스에서 읽는 선언**(`python-multiplatform-ksp` 의 `BindingPolicy.kt`)에 대한 규칙이다. 미리 컴파일된 jar 를 읽는 아티팩트 워커(`python-multiplatform-gradle-plugin`)는 같은 블랙리스트 정신을 따르되, 소스 없이 바이트코드/Kotlin 메타데이터에서 읽으므로 일부 행이 -다르다 — 아래 해당 행과 §"결정 이력" 에 표시했다. Kotlin 이름은 어느 쪽이든 그대로 노출되며 +다르다, 아래 해당 행과 §"결정 이력" 에 표시했다. Kotlin 이름은 어느 쪽이든 그대로 노출되며 (AGENTS.md §12.1) 이 정책에는 이름을 바꾸는 규칙이 없다. ## 원칙: 블랙리스트 @@ -44,15 +44,15 @@ class InternalUtility { } // 클래스 통째로 제외 | **companion object 멤버** | ✅ 클래스의 정적 메서드처럼 노출 | companion 객체 자체를 따로 노출할 필요는 없다 | | **`object` 선언** | ✅ companion 과 같은 모양 | 인스턴스가 하나뿐이므로 수신자가 없다. 생성자는 없다 | | **`interface`** | ✅ 멤버만 | 인스턴스는 없지만, 핸들로 건너온 Kotlin 객체를 인터페이스를 통해 호출할 수 있다. 생성자 없음 | -| **`enum class`** | ✅ 엔트리마다 정적 접근자 + `name`/`ordinal`/`valueOf` | 엔트리 이름은 `ReflectedClass.enumEntryNames` 로 함께 나간다 — Python 쪽 `enum.Enum` 미러의 재료 | +| **`enum class`** | ✅ 엔트리마다 정적 접근자 + `name`/`ordinal`/`valueOf` | 엔트리 이름은 `ReflectedClass.enumEntryNames` 로 함께 나간다, Python 쪽 `enum.Enum` 미러의 재료 | | **중첩 선언** | ✅ `Outer.Inner` 이름으로 | 노출되지 않는 바깥 선언에서 탐색이 멈춘다 | | **`annotation class`** | ❌ | 애노테이션을 읽으려면 런타임 리플렉션이 필요하고, Python 이 만든 인스턴스는 붙일 곳이 없다 | | **enum 의 `values()` / `entries`** | ❌ | 컬렉션을 돌려주는데 경계에 컬렉션 마샬링이 없다 | | **제네릭 선언** (`class Box`, `fun f`) | ❌ | 생성된 캐스트가 이름 붙일 타입이 없다: `args[0] as T` 는 unresolved reference | -| **abstract / sealed 클래스의 생성자** | ❌ | "Cannot create an instance of an abstract class" — 생성된 파일이 컴파일되지 않는다 | +| **abstract / sealed 클래스의 생성자** | ❌ | "Cannot create an instance of an abstract class", 생성된 파일이 컴파일되지 않는다 | | `private` / `protected` | ❌ | | | **`internal`** | ❌ | JVM 바이트코드에서는 name-mangling 된 public 이지만 의미론적으로는 비공개다. KSP 의 `Modifier.INTERNAL` 로 걸러낸다 | -| **확장 함수 (KSP, 소스)** | ❌ | 생성되는 엔트리는 `Array` 위의 람다이고 수신자를 둘 자리가 없다 (`BindingPolicy.isExposedFunctionShape` 이 `extensionReceiver != null` 을 거른다). 아티팩트 워커는 다르다 — 아래 | +| **확장 함수 (KSP, 소스)** | ❌ | 생성되는 엔트리는 `Array` 위의 람다이고 수신자를 둘 자리가 없다 (`BindingPolicy.isExposedFunctionShape` 이 `extensionReceiver != null` 을 거른다). 아티팩트 워커는 다르다, 아래 | | **확장 함수 (아티팩트 워커, 컴파일된 jar)** | ✅ 수신자 프록시의 메서드로 | `Modifier.padding(16).size(24)` 처럼 체인이 이어진다 (SPEC U-7, `docs/design/kotlin-extensions-in-python.md`) | | **소스 안의 `@Composable`** | ❌ (KSP) | 생성된 호출이 `@Composable` 컨텍스트 밖이라 컴파일되지 않는다. 컴파일된 jar 의 composable 은 워커가 바인딩한다 (SPEC B-6, `docs/design/ecosystem.md` §4) | @@ -68,8 +68,8 @@ class InternalUtility { } // 클래스 통째로 제외 | 대상 | 처리 | 근거 | |---|---|---| -| **`suspend` 함수** | **노출한다** — 비동기 호출 규약으로 | 생성 본문이 `PendingCall.start { }` 로 코루틴을 시작하고, 경계는 값(중단 없이 끝남) 또는 `asyncio.Future`(중단됨)를 돌려준다. Python 에서 `await` 한다 (SPEC U-5, [`upcall.md`](upcall.md)). 처음에는 "자동 제외"였다 — 아래 정정 참조. `suspend` **함수 타입** 값(`suspend (Long) -> Long`)은 불투명 `OBJECT` 핸들로 나간다 | -| **`inline` + `reified`** | 자동 제외 | 컴파일 후 reified 타입 정보가 사라진다. **실제 규칙은 이보다 넓다** — 타입 파라미터가 있는 선언은 `reified` 여부와 무관하게 전부 제외한다 | +| **`suspend` 함수** | **노출한다**, 비동기 호출 규약으로 | 생성 본문이 `PendingCall.start { }` 로 코루틴을 시작하고, 경계는 값(중단 없이 끝남) 또는 `asyncio.Future`(중단됨)를 돌려준다. Python 에서 `await` 한다 (SPEC U-5, [`upcall.md`](upcall.md)). 처음에는 "자동 제외"였다, 아래 정정 참조. `suspend` **함수 타입** 값(`suspend (Long) -> Long`)은 불투명 `OBJECT` 핸들로 나간다 | +| **`inline` + `reified`** | 자동 제외 | 컴파일 후 reified 타입 정보가 사라진다. **실제 규칙은 이보다 넓다**, 타입 파라미터가 있는 선언은 `reified` 여부와 무관하게 전부 제외한다 | | **값 클래스(value class)** | KSP: 별도 규칙 없음(`BindingPolicy.kt` 에 값 클래스 분기가 없다). 아티팩트 워커: 왕복을 지원하고, 허용 목록의 것(예: `Dp`)만 원시 값으로 쓸 수 있다 | JVM 에서 언박싱된 형태로 노출되어 케이스가 갈린다 (SPEC B-4, `ComposableValueClassSlotTest.kt`) | `suspend` 와 `reified` 는 KSP 에서 선언 수준으로 판별되므로 걸러내는 비용이 낮다. @@ -77,19 +77,19 @@ class InternalUtility { } // 클래스 통째로 제외 ## 결정 이력 블랙리스트 원칙은 설계 논의에서 직접 확정됐다. 세부 규칙(가시성, 확장 함수, 매핑 불가 타입, 애노테이션 -이름)은 그 논의에서 다루지 않았고, 이후 판단으로 정한 것이다 — 구현하면서 뒤집힐 수 있다. +이름)은 그 논의에서 다루지 않았고, 이후 판단으로 정한 것이다, 구현하면서 뒤집힐 수 있다. 특히 다음 둘은 재고 여지가 있다: -- **확장 함수 제외 (KSP)** — 소스 경로에서는 제외 상태 그대로다. 다만 이것이 풀린 것은 아티팩트 +- **확장 함수 제외 (KSP)**: 소스 경로에서는 제외 상태 그대로다. 다만 이것이 풀린 것은 아티팩트 워커 쪽이다: 확장 함수는 수신자 프록시의 메서드로 노출된다(SPEC U-7). KSP 가 생성한 프록시에서의 동작은 아직 테스트로 단언되지 않았다. -- **값 클래스** — 초안의 "등록하고 실패하게 둔다"는 구현되지 않았다. 워커가 값 클래스를 다루고 +- **값 클래스**: 초안의 "등록하고 실패하게 둔다"는 구현되지 않았다. 워커가 값 클래스를 다루고 (SPEC B-4), KSP 는 따로 걸러내지 않는다. ## 구현 시점의 정정 (2026-08) -위 표 중 두 줄은 구현하면서 넓어졌고, 그 근거는 **생성된 파일이 컴파일되지 않은 것**이다 — +위 표 중 두 줄은 구현하면서 넓어졌고, 그 근거는 **생성된 파일이 컴파일되지 않은 것**이다, 프로세서가 스스로 알아챌 수 있는 종류의 오류가 아니었다. - **제네릭은 `reified` 뿐 아니라 전부 제외한다.** 엔트리는 `Array` 위의 람다이므로 모든 @@ -104,6 +104,6 @@ class InternalUtility { } // 클래스 통째로 제외 자동 제외했다. 비동기 업콜(`PendingCall`)이 생긴 뒤 이 수식어는 제외가 아니라 **본문 형태를 고르는 신호**가 되었다 (`BindingPolicy.isSuspending`, `GeneratedSuspendTest`). - **추상·봉인 클래스는 클래스로는 노출하되 생성자만 뺀다** (`BindingPolicy.isConstructible`; `inner` - 클래스도 같다 — 바깥 인스턴스를 둘 자리가 없다). 표의 "abstract / sealed 클래스의 생성자" 행의 구현. + 클래스도 같다, 바깥 인스턴스를 둘 자리가 없다). 표의 "abstract / sealed 클래스의 생성자" 행의 구현. - **`internal set` 은 노출하지 않는다.** `private set` 은 생성 코드가 컴파일되지 않아서, `internal set` 은 컴파일은 되지만 노출 대상이 모듈 공개 API 이기 때문이다 (`BindingPolicy.isExposedSetter`). diff --git a/docs/design/downcall-design.md b/docs/design/downcall-design.md index fce470e97..549dfdf1f 100644 --- a/docs/design/downcall-design.md +++ b/docs/design/downcall-design.md @@ -1,4 +1,4 @@ -# 다운콜 설계 — Kotlin → Python +# 다운콜 설계, Kotlin → Python Kotlin 코드가 CPython Stable ABI 함수를 호출하는 방향. 반대 방향은 [`upcall.md`](upcall.md). @@ -8,7 +8,7 @@ Kotlin 코드가 CPython Stable ABI 함수를 호출하는 방향. 반대 방향 > `JAVA_LONG`), Android 는 `RegisterNatives` 로 등록한 JNI (함수별·API 레벨별 호출 규약), Native 는 > cinterop. 아래 "shape 트램폴린" 14종은 **어휘(vocabulary)로 구현되어 있으나**(`jvmMain/.../ShapeDowncalls.kt` > 의 `expect` + 데스크톱·Android·Native 구현) 330개 `actual` 을 `jvmMain` 한 벌로 합치는 통합에는 -> **쓰이지 않았다** — 소스에서 shape 함수를 부르는 곳은 데스크톱 `ProxyTypeFactory` 의 `tp_traverse`/ +> **쓰이지 않았다**, 소스에서 shape 함수를 부르는 곳은 데스크톱 `ProxyTypeFactory` 의 `tp_traverse`/ > `tp_free` 호출 두 곳뿐이다. 이 문서의 shape 설계 절은 그 계획과 근거의 기록이다. ## 시그니처 shape 조사 결과 @@ -28,7 +28,7 @@ Kotlin 코드가 CPython Stable ABI 함수를 호출하는 방향. 반대 방향 부동소수점을 쓰는 함수는 `PyLong_FromDouble`, `PyLong_AsDouble`, `PyFloat_FromDouble`, `PyFloat_AsDouble`, `PyFloat_GetMax`, `PyFloat_GetMin` 여섯 개가 전부다. -shape 를 가르는 기준은 arm64(AAPCS64)와 x86-64(SysV) 모두에서 같다 — **정수·포인터가 한 레지스터 +shape 를 가르는 기준은 arm64(AAPCS64)와 x86-64(SysV) 모두에서 같다, **정수·포인터가 한 레지스터 뱅크, 부동소수점이 다른 뱅크**를 쓰므로, (정수형 인자 수, 부동형 인자 수, 반환 종류) 가 shape 다. 혼합 시그니처가 0개라 트램폴린의 C 프로토타입이 단순해진다. @@ -36,11 +36,11 @@ shape 를 가르는 기준은 arm64(AAPCS64)와 x86-64(SysV) 모두에서 같다 ## 플랫폼별 메커니즘 -### iOS / androidNative — cinterop +### iOS / androidNative, cinterop Kotlin/Native 가 헤더를 읽어 직접 호출 코드를 낸다. **오버헤드가 없고 손댈 것도 없다.** 이미 최적이다. -### Desktop — Panama +### Desktop, Panama `desktopMain/.../Panama.kt` 가 `java.lang.foreign`(JDK 22+, 19~21 은 preview)과 `jdk.incubator.foreign`(JDK 16~18)을 **둘 다 리플렉션으로만** 접근한다. 어느 API 도 직접 import 하지 @@ -55,7 +55,7 @@ Kotlin/Native 가 헤더를 읽어 직접 호출 코드를 낸다. **오버헤 `.invoke(` 는 0곳이다 (AGENTS.md §16: 무조건 `invokeExact`). 리플렉션은 핸들을 **만들 때** 심볼당 한 번만 돈다. `Linker.Option.critical()` 은 쓰지 않는다 (`downcallHandle(..., emptyOptions)`). -### Android(ART) — shape 트램폴린 (구현됨, 단 주 경로는 아니다) +### Android(ART), shape 트램폴린 (구현됨, 단 주 경로는 아니다) ART 는 런타임에 임의 시그니처의 네이티브 호출을 만들 수 없다. 그래서 shape 마다 트램폴린을 **미리 컴파일**해두고, 대상 함수 포인터를 첫 인자로 받는다: @@ -80,12 +80,12 @@ fun downcallJ_J(fn: Long, a0: Long): Long = (`androidMain/README.md`). shape 트램폴린은 `ffiSymbol` 로 얻은 임의 주소를 부르는 일반 수단으로 남아 있다. -**LLVM JIT 은 필요 없다 — 다만 근거는 아래와 같다.** +**LLVM JIT 은 필요 없다, 다만 근거는 아래와 같다.** ART 에는 FFM 을 위한 VM 지원이 없다. HotSpot 은 `downcallHandle` 호출 시 VM 이 스텁을 기계어로 생성해 주지만, ART 는 해 주지 않는다. 그래서 임의의 `FunctionDescriptor` 에 대해 인자를 레지스터에 배치하고 대상을 호출하는 기계어를 **누군가는 만들어야 한다.** PanamaPort 는 그것을 런타임에 -`libLLVM.so` 로 만든다 — `_AndroidLinkerImpl.generateNativeDowncallStub`(292행)이 다운콜 경로에서 +`libLLVM.so` 로 만든다, `_AndroidLinkerImpl.generateNativeDowncallStub`(292행)이 다운콜 경로에서 **조건 없이** 호출되며(938행), 업콜도 `generateNativeUpcallStub`(954행)으로 같다. 주목할 점은 그 생성 스텁도 **대상 함수 포인터를 첫 인자로 받는다**는 것이다: @@ -96,7 +96,7 @@ stub_descriptor = stub_descriptor.insertArgumentLayouts(0, WORD); // leading fu ``` 우리 `downcallII_I(fn, a0, a1)` 과 같은 구조다. 차이는 **생성 시점**뿐이다 (PanamaPort 는 의존성으로 쓰지 -않는다 — AGENTS.md §12.9. 아래는 설계 근거로 읽은 분석이다; [`android-ffm-design.md`](../archive/android-ffm-design.md)): +않는다, AGENTS.md §12.9. 아래는 설계 근거로 읽은 분석이다; [`android-ffm-design.md`](../archive/android-ffm-design.md)): | | PanamaPort | 이 프로젝트 | |---|---|---| @@ -109,10 +109,10 @@ stub_descriptor = stub_descriptor.insertArgumentLayouts(0, WORD); // leading fu (이전 판에서 `BulkLinker.requireNativeStub` 이 `false` 를 반환하므로 참조 구현도 스텁 없이 직접 패치한다고 서술했으나 이는 오독이었다. `BulkLinker` 는 `Unsafe` 모듈에 있는 PanamaPort 자체 -부트스트랩 수단이며 — LLVM API 같은 내부 네이티브 함수를 바인딩한다 — `Core` 의 Linker 경로에서는 +부트스트랩 수단이며, LLVM API 같은 내부 네이티브 함수를 바인딩한다, `Core` 의 Linker 경로에서는 쓰이지 않는다.) -## JVM 통합 — Desktop + Android 를 한 벌로 (계획; 미수행) +## JVM 통합, Desktop + Android 를 한 벌로 (계획; 미수행) 계획은 이랬다. 당시 JVM 쪽은 330개 `actual` 이 **두 벌** 있고 플랫폼별 `bindings` 선언까지 더하면 손으로 유지하는 선언이 1,300개에 가까웠다. shape 를 도입하면 플랫폼별 표면이 14개로 줄고 330개 `actual` 은 @@ -147,14 +147,14 @@ Native Image 는 **FFM 다운콜을 빌드 타임에 등록**해야 한다. `ref 모든 `FunctionDescriptor` 가 그 메타데이터에 선언되어 있는지는 `ReachabilityMetadataTest.kt` 가 지킨다 (SPEC C-2). `JAVA_LONG`/shape 로 시그니처 종류가 한정되는 것이 등록을 단순하게 한다. -이것은 더 이상 "우선순위 낮음"이 아니다 — 업콜이 네이티브 이미지에서 동작해야 한다는 것은 이 저장소의 +이것은 더 이상 "우선순위 낮음"이 아니다, 업콜이 네이티브 이미지에서 동작해야 한다는 것은 이 저장소의 결정이다 (AGENTS.md §13). 절차와 마지막 검증 기록은 [`graal-native-image-verification.md`](../platforms/graal-native-image-verification.md) (SPEC U-6: 수동 검증, 이미지 자체를 테스트하는 자동화는 아직 없음). ## 스레드 상태 -다운콜은 전부 GIL(또는 free-threaded 빌드의 스레드 상태)을 요구한다 — `withGIL { }` +다운콜은 전부 GIL(또는 free-threaded 빌드의 스레드 상태)을 요구한다, `withGIL { }` (`python/multiplatform/ffi/GILScope.kt`). [`threading-and-abi.md`](threading-and-abi.md) 를 본다. --- @@ -168,7 +168,7 @@ Native Image 는 **FFM 다운콜을 빌드 타임에 등록**해야 한다. `ref 초기에는 `@CName` 으로 만든 C 함수(`JNIEnv`/`jclass` 를 받지 않음)를 일반 JNI 메서드처럼 이름으로 연결해서, ART 가 넘기는 `JNIEnv*`·`jclass` 때문에 **인자가 두 칸 밀렸다.** 지금은 `JNI_OnLoad` -(`artMain`)가 `RegisterNatives` 로 모든 `external fun` 을 등록한다 (367개 중 363개; 예외 4개는 의도적 — +(`artMain`)가 `RegisterNatives` 로 모든 `external fun` 을 등록한다 (367개 중 363개; 예외 4개는 의도적, ROADMAP §2). 이름 기반 연결은 API 26 에서 런타임을 abort 시키기도 한다. 초안의 진단·표는 [`../archive/downcall-design-android-first-diagnosis.md`](../archive/downcall-design-android-first-diagnosis.md). @@ -186,9 +186,9 @@ ROADMAP §2). 이름 기반 연결은 API 26 에서 런타임을 abort 시키기 | 함수 | critical | |---|---| | `PyList_Size`, `Py_IncRef`, `PyLong_FromLongLong` | 적용 가능 | -| `PyObject_Call`, `PyRun_String`, `PyImport_ImportModule` | **불가** — Python 코드를 실행하며 Kotlin 으로 되돌아올 수 있다 | +| `PyObject_Call`, `PyRun_String`, `PyImport_ImportModule` | **불가**, Python 코드를 실행하며 Kotlin 으로 되돌아올 수 있다 | -가장 뜨거운 호출들이 정확히 critical 을 못 쓰는 쪽이다. 그리고 **shape 는 이를 구분하지 못한다** — +가장 뜨거운 호출들이 정확히 critical 을 못 쓰는 쪽이다. 그리고 **shape 는 이를 구분하지 못한다**, `downcallII_I` 하나가 `PyList_GetItem`(가능)과 `PyObject_Call`(불가)을 함께 덮는다. 시그니처가 같아도 성격이 반대다. @@ -209,19 +209,19 @@ Desktop 과 Android 를 같은 방식으로 통일하려고 Android 에 FFM 을 Android 의 Panama 는 런타임인 척하는 라이브러리다. 같은 바닥 위에 계층이 더 있고 JIT 가 약하므로 Android 쪽이 더 빠를 수는 없다. 다만 **얼마나 다른지는 측정된 바 없다.** -우리 desktop 구현은 현재 `Linker.Option.critical()` 을 쓰지 않는다 — +우리 desktop 구현은 현재 `Linker.Option.critical()` 을 쓰지 않는다, `Panama.kt` 가 `downcallHandle(..., emptyOptions)` 로 기본 경로를 탄다. -### 조립 단위의 범위 — 정해졌다 +### 조립 단위의 범위, 정해졌다 -경계 통과가 비싸고 뜨거운 호출에는 critical 도 못 쓰므로 **통과 횟수 자체를 줄이는 것**이 접근이었다 — +경계 통과가 비싸고 뜨거운 호출에는 critical 도 못 쓰므로 **통과 횟수 자체를 줄이는 것**이 접근이었다, `bindings` 의 저수준 함수들을 바인더 레벨 연산으로 조립해 `artMain` 에서 내보내고 `androidMain` 은 그 굵은 단위를 한 번 부른다 (`artMain` 의 `asmGetAttr` 등; 조립 단위는 길고 업콜 가능성이 있으므로 일반 JNI, shape 계열은 짧고 콜백이 없으므로 규약을 함수별로 선택). 초안은 세 후보를 두고 결정을 미뤘다(바인더 API 를 `expect`/`actual` 로 올림 / `commonMain` 에 두고 Android 만 별도 경로 / `Python3` 수준만 조립). **택한 것은 어느 것도 아니다**: 조립은 **FFI 층의 `expect`/`actual`** -(`EmbedAPI`)에 두고 객체 모델은 공통으로 둔다 — 객체 모델은 `bindings` 를 참조하지 않는다 +(`EmbedAPI`)에 두고 객체 모델은 공통으로 둔다, 객체 모델은 `bindings` 를 참조하지 않는다 (`architecture.md` "Consequence for composed operations", `commonMain/README.md`). 그리고 **문자열을 나르는 연산에서는 조립보다 interning 이 더 싸고 일반적**이라 그쪽으로 갔다 ([`marshalling-design.md`](marshalling-design.md)); 조립이 남는 곳은 통과 횟수가 N 에 비례하는 일괄 @@ -236,8 +236,8 @@ project, and it contradicts the assumption the Android design was built on. ### Method -`echo0`, `echoFast` and `echoNormal` are the same one-line C body — `jlong f(jlong x) -{ return x; }` — registered three times under the three conventions +`echo0`, `echoFast` and `echoNormal` are the same one-line C body, `jlong f(jlong x) +{ return x; }`, registered three times under the three conventions (`artMain/cinterop/jni_onload.def`). The callee work is identical and negligible, so the difference between them is the transition and nothing else. A pure-Kotlin identity call is measured alongside as the floor and subtracted. @@ -245,7 +245,7 @@ measured alongside as the floor and subtracted. All variants are warmed up before any are timed, and rounds interleave all four, so drift hits them equally. Best-of-7 rounds, 2,000,000 iterations each. `JniOverheadBenchmark`. -### Result — net of the Kotlin floor, ns/call +### Result, net of the Kotlin floor, ns/call | convention | API 26 (Android 8.0) | API 34 (Android 14) | |---|---|---| @@ -255,7 +255,7 @@ hits them equally. Best-of-7 rounds, 2,000,000 iterations each. `JniOverheadBenc The two devices are inverted. On API 26 `@CriticalNative` is ~24x cheaper than ordinary JNI, which is what the design predicted. On API 34 it is the *most* expensive of the three -— 3x worse than ordinary JNI and 12x worse than `@FastNative`. +3x worse than ordinary JNI and 12x worse than `@FastNative`. ### What was ruled out @@ -267,12 +267,12 @@ JNI, which is what the design predicted. On API 34 it is the *most* expensive of 20,000 iterations instead of 2,000,000: API 34 critical stayed at 26.49 net. Batch size is not the explanation. - **Registration silently failing.** `argumentsArriveUnshifted` passes on both devices, so the - critical convention *is* in effect on API 34 — arguments arrive unshifted. It is correct + critical convention *is* in effect on API 34, arguments arrive unshifted. It is correct there, just slow. ### What is not known -Why API 34 is slow has not been established. The inversion is suspiciously symmetric — each +Why API 34 is slow has not been established. The inversion is suspiciously symmetric, each device looks like it honours exactly one of the two annotations on a fast path and routes the other through a generic trampoline that is correct but slow. That is a hypothesis, not a finding. @@ -286,10 +286,10 @@ measure on physical devices at both API levels.** If this holds on hardware, the current design is optimal at minSdk and worst-case on modern Android, which is backwards from where the users are. The fix is to pick the convention per API level. The annotation is compile-time, so that means declaring both variants and dispatching on -`Build.VERSION.SDK_INT` — a branch costing ~1-2ns to avoid ~22ns. (It held on hardware, below, and +`Build.VERSION.SDK_INT`, a branch costing ~1-2ns to avoid ~22ns. (It held on hardware, below, and was then built: `bindings.preferFastNative`.) -### Re-measured on physical hardware — the inversion is real +### Re-measured on physical hardware, the inversion is real The section above asked for physical-device confirmation before acting. Done: Samsung SM-X910 (Galaxy Tab S9 Ultra), Android 16 / API 36, arm64-v8a, 8 cores. It is not an @@ -307,13 +307,13 @@ Net of the Kotlin floor, ns/call: Three things are now settled. **The binding method is not the cause.** The hypothesis was that explicitly-registered -critical natives lose the fast path on modern ART while name-linked ones keep it — Google's +critical natives lose the fast path on modern ART while name-linked ones keep it, Google's advice to use RegisterNatives *before Android 12* reads as if the reverse holds after. So a name-linked `@CriticalNative` echo was added, identical in every other way. It is *slower* still: 66.80ns on API 34 and 76.56ns on API 36 against 24.35 and 46.68 for the registered one. Hypothesis rejected. `@CriticalNative` is simply expensive on modern ART. -**Name linking is not merely slower below Android 12 — it is fatal.** On API 26 that call +**Name linking is not merely slower below Android 12, it is fatal.** On API 26 that call aborted the ART runtime outright (`zygote64: runtime.cc:492] Runtime aborting...`) and took the instrumentation process with it. Google's guidance turns out to be a hard requirement, not a preference. The benchmark now guards that call behind `SDK_INT >= S`. @@ -325,13 +325,13 @@ underlying reason, the practical shape is unambiguous. ### Consequence: the current implementation is on the wrong path for real users The 11 migrated functions use `@CriticalNative` via RegisterNatives. That is the best -available choice at minSdk 26 and the *worst* on the hardware people actually carry — +available choice at minSdk 26 and the *worst* on the hardware people actually carry, 46.68ns where `@FastNative` costs 2.52ns, an 18x penalty. The annotation is resolved at compile time, so the convention cannot be switched at runtime for one declaration. The fix is to declare both variants and dispatch on `Build.VERSION.SDK_INT`, paying one predictable branch (~1-2ns) to avoid ~44ns. `@FastNative` -still receives JNIEnv and jclass, so its exports need those leading parameters — meaning two +still receives JNIEnv and jclass, so its exports need those leading parameters, meaning two export sets in artMain, not one. **Implemented** (`bindings.preferFastNative = SDK_INT >= 34`, `androidMain/.../bindings.kt`; the @@ -352,7 +352,7 @@ Measured across five API levels plus hardware. Net of the Kotlin floor, ns/call, | 34 (Android 14) | 24.35 | **1.97** | 7.81 | | 36 (Android 16, SM-X910 hardware) | 44.05 | **3.55** | 18.32 | -`@CriticalNative` is effectively free — at or below the measurement floor — from API 26 +`@CriticalNative` is effectively free, at or below the measurement floor, from API 26 through 33, then collapses at 34 and stays collapsed on hardware. `@FastNative` moves the other way: expensive through API 31, cheap from 33 onward. Two independent ART changes, and they do not happen at the same release. @@ -376,8 +376,8 @@ one place the data is closest. Three separate times, an instrumented run failed because the APK did not contain what the source tree said it should. `connectedDebugAndroidTest` does not reliably force `linkAndroidNative*` or the `copyAndroidPythonBinaries` / `copyAndroidPythonAssets` staging, -so a changed `.def` or a cleaned `build/` produces an APK holding a stale — or entirely -missing — `libmultiplatform_python3.14.so` and no `libpython3.14.so` beside it. The symptom is +so a changed `.def` or a cleaned `build/` produces an APK holding a stale, or entirely +missing, `libmultiplatform_python3.14.so` and no `libpython3.14.so` beside it. The symptom is `UnsatisfiedLinkError`, which reads like a code error and is not one. The copy tasks are now wired as dependencies of the assets/jniLibs merge tasks @@ -390,7 +390,7 @@ If an instrumented run fails with `UnsatisfiedLinkError`, run this first: ### Final rule: the convention is a per-function decision, and correctness comes first -Picking by API level is only half of it. The two fast conventions are not merely faster — +Picking by API level is only half of it. The two fast conventions are not merely faster, they change what the callee is allowed to do: - Both `@FastNative` and `@CriticalNative` **stop the garbage collector** for the duration of @@ -401,7 +401,7 @@ they change what the callee is allowed to do: Plenty of CPython entry points can execute arbitrary Python: a module's top-level code during import, a `__getattr__` or descriptor during attribute lookup, a `__del__` reached by dropping the last reference, `site.py` during start-up, `atexit` handlers during teardown. Those are -unbounded in time, and once Kotlin callables are exposed to Python they re-enter the JVM — +unbounded in time, and once Kotlin callables are exposed to Python they re-enter the JVM, which is exactly what `@CriticalNative` cannot support. This is the same reason `Linker.Option.critical()` is a per-downcall option in Panama rather than a global switch. @@ -409,17 +409,17 @@ So each function is classified, and the classification outranks the API-level ch | class | rule | of the 11 migrated | |---|---|---| -| **leaf** — cannot execute Python | fastest convention for the API level | `Py_IsInitialized`, `PyList_Size`, `PyErr_Occurred`, `Py_GetVersion`, `PyLong_FromLongLong` | -| **re-entrant** — may run Python, may upcall, may block | **ordinary JNI, always** | `Py_Initialize`, `Py_Finalize`, `PyErr_Clear`, `PyRun_SimpleString`, `PyImport_ImportModule`, `PyObject_GetAttrString` | +| **leaf**, cannot execute Python | fastest convention for the API level | `Py_IsInitialized`, `PyList_Size`, `PyErr_Occurred`, `Py_GetVersion`, `PyLong_FromLongLong` | +| **re-entrant**, may run Python, may upcall, may block | **ordinary JNI, always** | `Py_Initialize`, `Py_Finalize`, `PyErr_Clear`, `PyRun_SimpleString`, `PyImport_ImportModule`, `PyObject_GetAttrString` | `PyErr_Clear` looks like a leaf and is not: clearing the error drops the last reference to the exception, and that can run a Python `__del__`. `PyObject_GetAttrString` looks like a field read and is not, for the same kind of reason. When in doubt the call goes on the ordinary -path — a wrong guess there costs nanoseconds, while a wrong guess the other way is a crash +path, a wrong guess there costs nanoseconds, while a wrong guess the other way is a crash once upcalls exist. Losing the fast path on the re-entrant half is not the tragedy it appears. Those calls do real -work — importing a module or running a statement dwarfs a 40ns transition — whereas the leaf +work, importing a module or running a statement dwarfs a 40ns transition, whereas the leaf calls, where the transition genuinely dominates, are exactly the ones that keep it. ## Desktop vs Android, measured @@ -428,7 +428,7 @@ calls, where the transition genuinely dominates, are exactly the ones that keep > row is corrected by "Fixed: 1015.95 ns -> 2.65 ns" and "Desktop vs Android, corrected" further down. Same benchmark shape on both sides: same warmup, same iteration count, best-of-7, a -pure-Kotlin identity call subtracted as the floor, and the same real C API call — +pure-Kotlin identity call subtracted as the floor, and the same real C API call, `PyList_Size` on `sys.path`. | | floor | `PyList_Size` | net | @@ -441,8 +441,8 @@ Desktop is roughly **140x more expensive per call than Android hardware**, and t worse than it looks: the host is about 9x faster at the floor (0.33ns vs 3.08ns), so the FFI call is slower on the machine that is faster at everything else. -The assumption running through this document — that Android is the platform with the -overhead problem and desktop is the reference — is backwards. Desktop is currently the +The assumption running through this document, that Android is the platform with the +overhead problem and desktop is the reference, is backwards. Desktop is currently the slowest FFI path in the project by two orders of magnitude. ### Why @@ -459,7 +459,7 @@ the result on every call. Worse, the argument and return filters are themselves The backend was written reflectively so the code would compile against `java.lang.foreign` and `jdk.incubator.foreign` on any JDK, which was the right call for portability and the wrong one for the hot path. The portability requirement does not extend to the per-call path -— only to how handles are *created*. +only to how handles are *created*. ### Consequence @@ -494,7 +494,7 @@ Measured on the same machine, same benchmark: | `PyList_Size` on `sys.path` | 1015.95 ns/call | **2.65 ns/call** | | net of the Kotlin floor | 1015.61 ns | **2.01 ns** | -About 380x, and desktop goes from the slowest FFI path in the project to the fastest — 2.65ns +About 380x, and desktop goes from the slowest FFI path in the project to the fastest, 2.65ns against 7.47ns on Android hardware, on a host roughly 9x faster at the floor, so the two are now in the same regime rather than separated by two orders of magnitude. @@ -512,7 +512,7 @@ Until now the modern-Android numbers came from a Samsung tablet while the older emulators on the M1 host, so "API 36 is slowest" mixed two variables. Running API 36 on an emulator settles it. -Same API level, different machine — real `PyList_Size` call: +Same API level, different machine, real `PyList_Size` call: | API 36 | floor | `@CriticalNative` | `@FastNative` | ordinary JNI | |---|---|---|---|---| @@ -523,7 +523,7 @@ Hardware is 1.5-2x the emulator across every row, and the floor moves by the sam (3.22 vs 2.20). That is the tablet's core being slower than the host, not anything about API 36. The earlier reading that "API 36 is the slow one" was partly this artefact. -Same machine, different API level — emulators on the M1 host only: +Same machine, different API level, emulators on the M1 host only: | | `@CriticalNative` | `@FastNative` | |---|---|---| @@ -533,7 +533,7 @@ Same machine, different API level — emulators on the M1 host only: The collapse is still real and still API-driven: `@CriticalNative` goes from free at 33 to 21.72ns at 36 with the hardware held constant. So the crossover conclusion and the dispatch -threshold stand — hardware exaggerated the gap but did not create it. +threshold stand, hardware exaggerated the gap but did not create it. ### Desktop vs Android, corrected @@ -551,7 +551,7 @@ comparison implied. Both are now in the same regime, which is the part that matt ## Composition: measured -Per-crossing cost has bottomed out — 2.65ns on desktop, 1.66-7.24ns on Android depending on +Per-crossing cost has bottomed out, 2.65ns on desktop, 1.66-7.24ns on Android depending on API level and hardware. The only lever left is crossing fewer times, which is the structure the Android path was originally built around: `artMain` assembling a whole binder operation and `androidMain` calling that assembly once, rather than `commonMain` driving raw C API @@ -575,7 +575,7 @@ calls one crossing at a time. **Composition wins, but not purely by removing crossings.** Four crossings at ~7ns is 28ns, which does not explain 3929ns versus 713ns. The per-call `getAttr` allocates a C string -through `ffiAllocUtf8` — a malloc, a copy out of the jstring, and a matching free — where the +through `ffiAllocUtf8`, a malloc, a copy out of the jstring, and a matching free, where the composed version borrows the jstring directly with `GetStringUTFChars` and releases it. Most of the win is redundant work that disappears when the operation is expressed once on the native side, and the crossing count is the smaller part. That is still a real argument for @@ -585,15 +585,15 @@ composing, but the mechanism is not the one the crossing-count framing suggests. 1000 elements on hardware. That is the case the current design handles worst, because `commonMain` drives it element by element. -**API 26 barely benefits from composing the bulk case** — 1.2x. Its per-element crossings run +**API 26 barely benefits from composing the bulk case**, 1.2x. Its per-element crossings run under `@CriticalNative`, which is free at that API level, so there was little to remove. This is the mirror image of the convention result: composition pays most exactly where the per-crossing cost is highest, which is modern Android, which is where the users are. ### Where this leaves the design -`PyObject` was `expect`/`actual` with an Android `actual` marked `external` — the hook for -exactly this — until commit 0fae961a (2025-12-20) folded it into a single `commonMain` class. +`PyObject` was `expect`/`actual` with an Android `actual` marked `external`, the hook for +exactly this, until commit 0fae961a (2025-12-20) folded it into a single `commonMain` class. That commit's real subject was moving `PyAutoCloseable` to per-platform implementations, which was right and should stand; losing the composition hook was collateral. The resolution was **not** to restore `expect`/`actual` on `PyObject`: composition lives in the FFI layer (`architecture.md`), and @@ -605,7 +605,7 @@ that is a much smaller loss on desktop than the same gap would be on Android. ## One downcall, across all platforms -The upcall side has §7.1 "One upcall, per platform" in [`upcall.md`](upcall.md#71-one-upcall-per-platform) — one shared +The upcall side has §7.1 "One upcall, per platform" in [`upcall.md`](upcall.md#71-one-upcall-per-platform), one shared test, run on every target, min–max over several runs, quoted rows marked as quotes. The downcall side had no equivalent: the benchmarks that exist are real and each one answers a question, but they are scattered across five files with no shared shape, so "how expensive is a downcall on @@ -623,7 +623,7 @@ platform X" had no single table to read. | `androidInstrumentedTest/.../CompositionBenchmark.kt` | `androidInstrumentedTest` | Per-call crossings vs one composed `artMain` call, for `getAttr` and `list → array`, plus a direct-buffer marshalling variant | Android/ART only, needs a device or emulator | Two more files live next to these and share the word "overhead" or "Upcall" in their name but are -**not** downcall benchmarks — noted here only so a future reader does not double-count them: +**not** downcall benchmarks, noted here only so a future reader does not double-count them: `commonTest/.../reflection/UpcallOverheadTest.kt` prices the upcall dispatch table (`UpcallTable.resolve` vs a cached handle), and `androidInstrumentedTest/.../UpcallOverheadTest.kt` prices the Android upcall attach. Both are upcall-side and already covered by `upcall.md`. @@ -631,36 +631,36 @@ prices the Android upcall attach. Both are upcall-side and already covered by `u `BenchmarkTest` reaches every one of the six execution paths KGP registers for this module (`desktopTest`, `iosSimulatorArm64Test`, `iosX64Test`, `wasmJsNodeTest`, `androidInstrumentedTest`/ `connectedAndroidTest`, `androidNativeArm64Test`+`androidNativeX64Test`) because it sits in -`commonTest`, which every one of those depends on transitively. It is confirmed to run — not just -compile — on three of the six this pass: desktop, the iOS simulator, and wasmJs (see Validation +`commonTest`, which every one of those depends on transitively. It is confirmed to run, not just +compile, on three of the six this pass: desktop, the iOS simulator, and wasmJs (see Validation below). `iosX64Test` has no practical run path on Apple Silicon and nobody targets it; the Android/ART and androidNative rows need a connected device or emulator, which this pass did not have available. ### A shared table, picked to match the upcall table's columns -The upcall table's denominator column is `PyObject_CallObject` on a Python `def` — "a downcall of -the same shape" — plus `Py_IncRef`/`Py_DecRef` and an empty `withPython` scope as the two +The upcall table's denominator column is `PyObject_CallObject` on a Python `def`, "a downcall of +the same shape", plus `Py_IncRef`/`Py_DecRef` and an empty `withPython` scope as the two boundary-cost brackets. Those three, plus one string-marshalling call, are exactly what `UpcallBoundaryCostTest`'s `measureDowncalls()` (`commonTest`) already records **in the same run as -the upcall it prices** — so pulling them out as their own table is not a new measurement, it is the +the upcall it prices**, so pulling them out as their own table is not a new measurement, it is the existing "comparison basis" rows read on their own. The fourth column, string marshalling, comes from `BenchmarkTest.testStringMarshalling`'s 8-character row, run in the same suite execution. -Desktop, iOS simulator and wasmJs are measured fresh here — three full-suite runs +Desktop, iOS simulator and wasmJs are measured fresh here, three full-suite runs (`:desktopTest :iosSimulatorArm64Test :wasmJsNodeTest`), same command as Validation below, each with `build/test-results` cleared first so a task cannot report a stale XML. All three runs matched the baseline exactly (360/0/1, 330/0/0, 340/0/0). Ranges are min–max over three runs for the first three columns; the string-marshalling column is min–max over two of those three runs (the first -run's `BenchmarkTest` XML was not preserved before the second run overwrote it — its +run's `BenchmarkTest` XML was not preserved before the second run overwrote it, its `UpcallBoundaryCostTest` numbers were captured from console output before that happened, which is why that row has three runs and this one has two). -androidNative and Android/ART rows are **quoted from `upcall.md`**, not re-measured — marked +androidNative and Android/ART rows are **quoted from `upcall.md`**, not re-measured, marked † below, same convention that document uses for its own quoted rows. Cells with no prior measurement of this exact quantity are left blank rather than approximated from a different one -(e.g. `CompositionBenchmark`'s composed `getAttr` is a different quantity — four crossings folded -into one — not a bare `PyUnicode_FromString`). +(e.g. `CompositionBenchmark`'s composed `getAttr` is a different quantity, four crossings folded +into one, not a bare `PyUnicode_FromString`). > **Four of this table's seven rows are quotes from another document and one column of it is > superseded (below), which is what happens to a table assembled by hand.** The same four columns, @@ -734,7 +734,7 @@ Each platform-only file above measures something the others cannot, and none of the four shared columns: - **Desktop**: the Panama `invoke` → `invokeExact` migration (`DesktopOverheadBenchmark`, - 1015.95 ns → 2.65 ns for `PyList_Size`) is a desktop-only story — no other platform has an + 1015.95 ns → 2.65 ns for `PyList_Size`) is a desktop-only story, no other platform has an equivalent adaptation-vs-exact distinction. - **Android/ART**: `JniOverheadBenchmark`'s ordinary/`@FastNative`/`@CriticalNative` comparison and `CompositionBenchmark`'s per-call-vs-composed figures are both `artMain`/`androidMain` concepts @@ -743,7 +743,7 @@ the four shared columns: wasm-linear-memory concept; the other platforms marshal strings through Panama or JNI, not through an explicit intern cache. -These are not merged into the shared table, and the files are not merged or deleted — the request +These are not merged into the shared table, and the files are not merged or deleted, the request for this pass was a table, not a consolidation, and each file's platform-specific framing (its own warmup counts, its own comparison baselines) is part of what makes its own number meaningful. @@ -757,7 +757,7 @@ demonstrably present in the freshly linked `.so`. The cause was in `build.gradle } A bare `copy {}` inside a `configure {}` block executes while Gradle is configuring the build, -not when the link task runs — so every build staged the *previous* build's library into +not when the link task runs, so every build staged the *previous* build's library into `jniLibs`. Any change to the `.def` or to native sources needed two full builds before it reached the device, and in between the failure looked exactly like a code bug. Moved into `doLast`, so it stages what the link actually produced. diff --git a/docs/design/ecosystem.md b/docs/design/ecosystem.md index fa04ccad2..9f9a82e9f 100644 --- a/docs/design/ecosystem.md +++ b/docs/design/ecosystem.md @@ -1,8 +1,8 @@ # The repositories around this one, and what each owes `/Volumes/macMini/thisisthepy` holds several repositories that are meant to compose into one -product. Until now none of them referenced the others in writing — this repository's `docs/roadmap/ROADMAP.md` -mentions `pypackpack`, `toolchain`, `pythonx` and `chaquopy` exactly zero times — so this file +product. Until now none of them referenced the others in writing, this repository's `docs/roadmap/ROADMAP.md` +mentions `pypackpack`, `toolchain`, `pythonx` and `chaquopy` exactly zero times, so this file records the intended relationships, what each repository actually contains today, and what has to be built before the parts can meet. @@ -26,7 +26,7 @@ than observed are marked as such. pythonx-compose Compose bound into Python, so a UI can be written in Python. PyREPL reference implementation for turning a Kotlin namespace into `.pyi` stubs, - and — see below — the actual origin of `toolchain`. + and, see below, the actual origin of `toolchain`. The division of labour that matters: **ppp owns the work, toolchain owns the Gradle vocabulary, this repository owns the language boundary.** A feature that acquires a Python distribution or @@ -46,14 +46,14 @@ Working: `init`, `python use/list/find/install/uninstall`, `package add/remove/s `target list/add/remove`, per-package dependency operations with platform markers, and `build` driving Meson for C/C++. -Placeholder — verified by line count, all two to four lines: the whole `bundle/` tree, the whole +Placeholder, verified by line count, all two to four lines: the whole `bundle/` tree, the whole `deploy/` tree, every compile backend except Meson (Clang, MSVC, NDK, XCode, Emscripten, Cargo), and all of `compile/middleware/{external,transcompile,minification}`. It is already designed to be called from Gradle. `dependency/frontend/BaseInterface.kt` declares `enum FrontendType { CLI, GRADLE }` and `Gradle.kt` returns the same middleware the CLI uses. The -stable surface is `dependency/middleware/BaseInterface.kt` — `initProject`, `addDependencies`, -`syncDependencies`, `addTargets`, `installPythonVersion`, and so on — plus +stable surface is `dependency/middleware/BaseInterface.kt`, `initProject`, `addDependencies`, +`syncDependencies`, `addTargets`, `installPythonVersion`, and so on, plus `compile/middleware/BaseInterface.kt`'s `compile`. **But `packpack/build.gradle.kts` applies no `maven-publish` and declares no publication.** Nothing @@ -65,23 +65,23 @@ cannot be implemented at all. A Kotlin Gradle plugin, id `org.thisisthepy.python.multiplatform`, about 1,240 lines. It registers a `python` extension and three tasks. What the tasks do today: copy a local folder into `src/main/assets/python`, copy `build/pythonLibraries` into `build/pythonBundle/libs`, zip that, -and shell out `uv install -r` — which is not a valid uv command. +and shell out `uv install -r`, which is not a valid uv command. **`toolchain` shares git history with `PyREPL`.** Commits `f806656`, `5f3f932`, `1b32d03`, `654ef7c` exist in both with identical hashes; `dba17dd` extracted PyREPL's meta-generation out of `app/build.gradle.kts` into `toolchain/.../meta/createMetaClass.kt`. So the pyi generator to study -is already here, in a mutated form — see §4. +is already here, in a mutated form, see §4. The target API is `(플러그인예시)build.gradle.kts` at the repo root: 276 lines describing `compileSdk`, per-platform declarations, `packaging { embedLevel, hotReload, codePush }`, `buildTypes { compileLevel }`, `projectFlavors`, `buildFeatures { metaclass, compose }`, and source -sets carrying both `implementation("pyzmq")` and `integration("pycomposeui")` — the latter being a +sets carrying both `implementation("pyzmq")` and `integration("pycomposeui")`, the latter being a Kotlin-dependent Python package that needs a `KLIBDEPENS` file in its wheel. Of that spec the DSL data classes exist for roughly 60%, and **none of `hotReload`, `codePush`, `buildTypes`, `projectFlavors`, `buildFeatures`, `metaDirs`, `libDirs` or `integration()` is read by any task.** `PythonPlugin.kt` consumes only `sourceSets.*.dependencies.implementations`. -`dsl/PythonConfiguration.kt` is dead code — its extension is never registered. +`dsl/PythonConfiguration.kt` is dead code, its extension is never registered. `toolchain/usage-example/` does not apply the plugin, has no `python { }` block, and is commented out of `settings.gradle.kts`. **There is currently no executable definition of the target API.** @@ -96,7 +96,7 @@ with uv, crossenv and meson instead of kivy recipes. Two generations coexist, and the one the notebook demonstrates is in neither of them. `pythonx/compose/` is the chaquopy generation: a `Composable` decorator class, `ComposeApp`, and -material3 wrappers — 37 files of which 28 are empty. It binds through `jclass(...)` and +material3 wrappers, 37 files of which 28 are empty. It binds through `jclass(...)` and `from java import jclass`, and it reaches **AndroidX directly**: from androidx.compose.material3 import TextKt @@ -121,7 +121,7 @@ The Kotlin-to-pyi converter lives inline in `app/build.gradle.kts`, 627 lines. I source set's resolved artifacts, reads the jars with **ASM** (not kotlinx-metadata), and writes one `__init__.pyi` per Java package under `src//generated/meta/`, with `...` bodies and a JNI-ish type map. It drops private members, ``, `Companion`, and **any member whose name -contains `-`** — that is, it discards the mangled ones rather than scanning for them. Collisions +contains `-`**, that is, it discards the mangled ones rather than scanning for them. Collisions between `commonMain` and a platform source set are resolved by renaming both and re-exporting. It is stubs only; the runtime underneath is chaquopy `jclass`. Its purpose is IDE completion. @@ -134,7 +134,7 @@ keep.** ### Not part of this chain `Gemstone` belongs to a different organisation and does not depend on any of these; its README says -it is *scheduled* to adopt Python Multiplatform. `reference/` and `cpython/` are not repositories — +it is *scheduled* to adopt Python Multiplatform. `reference/` and `cpython/` are not repositories, vendored material and prebuilt CPython artefacts. --- @@ -143,10 +143,11 @@ vendored material and prebuilt CPython artefacts. `UI.ipynb` imports `pythonx.compose.runtime`, `pythonx.compose.material3`, `pythonx.compose.ui` and `pythonx.compose.layout`, and documents signatures like `Text(text, color, font_size)`, -`Button(onclick, …)` — quoted as the notebook writes it; the 2024 library spells that parameter +`Button(onclick, …)`, quoted as the notebook writes it; the 2024 library spells that parameter `on_click` (`docs/archive/pythonx-adapter-design.md` §3 measured both). Which Python spelling `pythonx` uses is pythonx-compose's decision: this repository's binder exposes the Kotlin name -`onClick` and renames nothing (`docs/INTENT.md` §2.2) — plus `Column/Row/Spacer/TextField`. It also +`onClick` and its Pythonic alias `on_click` on the Kotlin-named module, and renames no namespace +(`docs/INTENT.md` §2.2, SPEC U-12) - plus `Column/Row/Spacer/TextField`. It also requires `main.App` as a live object, `App.messages.getValue()/setValue()`, and `main.App.update(NewComposable)` for hot-swapping the UI from a Jupyter cell. @@ -172,14 +173,14 @@ handling at all. Every widget in `pycomposeui` is `@Composable`. Nothing in `pyt leave chaquopy until this is designed. *(read from source; not runtime-verified.)* This is the smallest and most blocking item, and it is entirely inside this repository. -> **Closed — see `docs/archive/pythonx-adapter-design.md` §5.6.** `Text('hi')` written in Python now draws -> through the real `androidx.compose.material3.Text` (`:ksp-fixtures:compose`'s +> **Closed, see `docs/archive/pythonx-adapter-design.md` §5.6.** `Text('hi')` written in Python now draws +> through the real `androidx.compose.material3.Text` (`:python-multiplatform-compose:fixtures:compose`'s > `ComposableRenderTest`: 71 non-background pixels against 0 for an empty body). The paragraph above > is right that a *generated Kotlin* entry for a widget cannot compile, and that is not the route > taken: an artefact composable's call site is emitted as **bytecode**, with `$composer`, `$changed` > and `$default` exposed as ordinary slots, and the binding layer (`python_multiplatform.binding`, > `PythonxAdapter.kt`) computes the mask. `BindingPolicy` does now -> have `@Composable` handling and what it does is **decline** — a composable in the consumer's *own +> have `@Composable` handling and what it does is **decline**, a composable in the consumer's *own > source* has no compiled signature to call yet, which is a different problem from one in a jar. **2. `packpack` must be publishable.** One file. Without it the stated architecture is @@ -192,7 +193,7 @@ this exists: generated modules are injected into `sys.modules` under their Kotli name. What is missing is a `sys.meta_path` finder so a name resolves on demand rather than only after an eager `install()`, the `pythonx` prefix, and generated `.pyi` beside it. -> **Current (2026-10-03):** the finder exists — the binding layer `python_multiplatform.binding` +> **Current (2026-10-03):** the finder exists, the binding layer `python_multiplatform.binding` > installs a `sys.meta_path` finder that makes Kotlin-named modules importable on demand > (`_Finder`/`_Loader` in `PythonxAdapter.kt`; SPEC U-8). The `pythonx` prefix is **not** this > repository's to provide: the binder creates no `pythonx` module, and `pythonx` is a real package in @@ -223,9 +224,9 @@ This section documents the current state, language/build system/distribution art ### `PythonMultiplatform` (this repository) - **Observed Current State**: - - Embedded CPython FFI binder (`python-multiplatform/src/commonMain/kotlin/.../EmbedAPI.kt`, platform implementations in `EmbedAPI.desktop.kt`, `bindings.kt`, JNI/Panama/cinterop). **Correction, 2026-08-17: this line used to say "CPython 3.13" — stale.** `gradle.properties`'s `pythonVersion` (the single source of truth, read into `Versions.currentVersion` at `python-multiplatform/src/commonMain/kotlin/python/multiplatform/Versions.kt`) is `3.14.7`, and every platform's *observed runtime* agrees: desktop prints `runtime : 3.14.7` (`4ed98143:docs/roadmap/ROADMAP.md:2063`), Android prints `3.14.7` on both `pmp_api36` and `pmp_api26` (`4ed98143:docs/roadmap/ROADMAP.md:2073`), wasmJs prints `3.14.2` — its own, separately-pinned Emscripten build, not this line's concern (`docs/platforms/wasm-design.md`). See the version-mismatch check below. + - Embedded CPython FFI binder (`python-multiplatform/src/commonMain/kotlin/.../EmbedAPI.kt`, platform implementations in `EmbedAPI.desktop.kt`, `bindings.kt`, JNI/Panama/cinterop). **Correction, 2026-08-17: this line used to say "CPython 3.13", stale.** `gradle.properties`'s `pythonVersion` (the single source of truth, read into `Versions.currentVersion` at `python-multiplatform/src/commonMain/kotlin/python/multiplatform/Versions.kt`) is `3.14.7`, and every platform's *observed runtime* agrees: desktop prints `runtime : 3.14.7` (`4ed98143:docs/roadmap/ROADMAP.md:2063`), Android prints `3.14.7` on both `pmp_api36` and `pmp_api26` (`4ed98143:docs/roadmap/ROADMAP.md:2073`), wasmJs prints `3.14.2`, its own, separately-pinned Emscripten build, not this line's concern (`docs/platforms/wasm-design.md`). See the version-mismatch check below. - Kotlin object model hierarchy (`python-multiplatform/src/commonMain/kotlin/.../PyObject.kt` and wrappers). - - Code generators: KSP processor (`python-multiplatform-ksp/`) producing `FunctionTableFragment`s, and Gradle plugin (`python-multiplatform-gradle-plugin/src/main/kotlin/.../artifact/ArtifactScanner.kt`, `PythonArtifactBindingsTask.kt`; *corrected 2026-10-03 — there is no `ArtifactBindingGenerator.kt`*) scanning resolved dependency jars (ASM + `kotlin-metadata-jvm`) and klibs for `ArtifactTable` fragments, plus `.pyi` stubs under Kotlin module paths (`stubs/PythonStubsTask.kt`). + - Code generators: KSP processor (`python-multiplatform-ksp/`) producing `FunctionTableFragment`s, and Gradle plugin (`python-multiplatform-gradle-plugin/src/main/kotlin/.../artifact/ArtifactScanner.kt`, `PythonArtifactBindingsTask.kt`; *corrected 2026-10-03, there is no `ArtifactBindingGenerator.kt`*) scanning resolved dependency jars (ASM + `kotlin-metadata-jvm`) and klibs for `ArtifactTable` fragments, plus `.pyi` stubs under Kotlin module paths (`stubs/PythonStubsTask.kt`). - Multiplatform target support: Desktop JVM/Panama, Android JNI, iOS Native Cinterop, androidNativeArm64 (`:python-multiplatform:compileKotlinAndroidNativeArm64`). - CPython binary download and SHA-256 lockfile / Sigstore verification (`python-checksums.properties`, `python-multiplatform/build.gradle.kts`). - **Language / Build System / Distribution**: @@ -236,19 +237,19 @@ This section documents the current state, language/build system/distribution art - Core language boundary and binder between Kotlin Multiplatform and CPython. - Manages low-level FFI, object reference handles, upcall/downcall function tables, GIL lifecycle, and proxy injection into CPython `sys.modules`. - **Gap to Goal** (status notes added 2026-10-03, read from code and SPEC, not re-run): - - `@Composable`-capable callable shape: `ExposedCallable` typed `(Array) -> Any?` cannot invoke `@Composable` functions taking synthetic `$composer` / `$changed` parameters. Needs dedicated `CallableKind` or opt-in annotation handling. — *Closed on desktop by bytecode thunks (`ComposableThunks.kt`), SPEC B-6; Android, iOS and wasm planned (SPEC N-2).* - - Python-side `sys.meta_path` finder for lazy import resolution of Kotlin namespaces ~~(`pythonx.*` or FQCNs)~~ upon import. — *Exists for Kotlin FQNs in `python_multiplatform.binding` (SPEC U-8). `pythonx.*` is not the binder's to resolve (`AGENTS.md` §12.2).* - - Opaque Kotlin object round-tripping for Compose `Composer` (`HandleTable` and `ObjectReference` exist, but runtime hand-off to Python needs verification). — *Done on desktop: one hand-written `@Composable` pushes the live composer as a handle onto the binding layer's stack (`push_composer`), proven by the `ksp-fixtures/compose` render tests.* - - Lifetime and storage of Python callables passed into Kotlin across Compose recompositions (`content=lambda: ...`, `onClick=...`). — *Partly addressed: `PythonCallableScope` (`PythonCallables.kt`) holds the callables a composition hands to Kotlin and is closed by a Compose-side `RememberObserver` in `onForgotten`; several hand-written fixture wrappers (`pythonDraggable`, `pythonComposed`, the swipe/anchor wrappers) still leak their callbacks by design, pinned by `DraggableLeakTest`. Not verified here beyond reading the sources.* + - `@Composable`-capable callable shape: `ExposedCallable` typed `(Array) -> Any?` cannot invoke `@Composable` functions taking synthetic `$composer` / `$changed` parameters. Needs dedicated `CallableKind` or opt-in annotation handling., *Closed on desktop by bytecode thunks (`ComposableThunks.kt`), SPEC B-6; Android, iOS and wasm planned (SPEC N-2).* + - Python-side `sys.meta_path` finder for lazy import resolution of Kotlin namespaces ~~(`pythonx.*` or FQCNs)~~ upon import., *Exists for Kotlin FQNs in `python_multiplatform.binding` (SPEC U-8). `pythonx.*` is not the binder's to resolve (`AGENTS.md` §12.2).* + - Opaque Kotlin object round-tripping for Compose `Composer` (`HandleTable` and `ObjectReference` exist, but runtime hand-off to Python needs verification)., *Done on desktop: one hand-written `@Composable` pushes the live composer as a handle onto the binding layer's stack (`push_composer`), proven by the `python-multiplatform-compose/fixtures/compose` render tests.* + - Lifetime and storage of Python callables passed into Kotlin across Compose recompositions (`content=lambda: ...`, `onClick=...`)., *Partly addressed: `PythonCallableScope` (`PythonCallables.kt`) holds the callables a composition hands to Kotlin and is closed by a Compose-side `RememberObserver` in `onForgotten`; several hand-written fixture wrappers (`pythonDraggable`, `pythonComposed`, the swipe/anchor wrappers) still leak their callbacks by design, pinned by `DraggableLeakTest`. Not verified here beyond reading the sources.* - Retire `stagePythonHome` in favor of `pypackpack`'s Python distribution management. - Hand `stageWasmBrowserRuntime` logic to `toolchain`. - **Dependency Direction**: - `PythonMultiplatform` has no dependencies on other repos in the ecosystem. - `pythonx-compose` and consumer applications depend on `PythonMultiplatform` runtime and its Gradle/KSP bindings plugin. - **Unverified**: - - Behavior of WASM browser runtime under production web bundlers. (*2026-10-03:* the former `wasm-experiment/` is folded into the library — `wasmJsMain`/`wasmJsTest`, CPython built by `tools/wasm/build-cpython.sh` — commit `6e54f617`; the bundler question is unchanged.) + - Behavior of WASM browser runtime under production web bundlers. (*2026-10-03:* the former `wasm-experiment/` is folded into the library, `wasmJsMain`/`wasmJsTest`, CPython built by `python-multiplatform/scripts/wasm/build-cpython.sh`, commit `6e54f617`; the bundler question is unchanged.) -**Version check (2026-08-17): "3.14 runtime vs. 3.13 header, never checked" — could not find the +**Version check (2026-08-17): "3.14 runtime vs. 3.13 header, never checked", could not find the sentence, checked the claim anyway.** The literal sentence was searched for across every `.md` file in this repository (`grep -rn` for Korean and English phrasings of "nobody/no one checked", "never verified", "mismatch", combined with "header"/"헤더" and "runtime"/"런타임") and in the git history @@ -256,33 +257,31 @@ of this file; it does not appear anywhere, verbatim or paraphrased. The nearest this section's own now-corrected "CPython 3.13" line above (stale since the version became configurable) and `4ed98143:docs/roadmap/ROADMAP.md:2033`, which already fixed a test fixture that hardcoded the string "CPython 3.13" while `pythonVersion` had moved to 3.14.7. Neither is the sentence quoted, and neither -left the underlying question — do headers and runtime actually agree, per platform — checked. That +left the underlying question, do headers and runtime actually agree, per platform, checked. That question was checked here, freshly, platform by platform: | platform | header source | runtime source | agree? | how checked | |---|---|---|---|---| -| desktop | none — Panama binds by symbol name at runtime (`java.lang.foreign.Linker`/`SymbolLookup`), no C header is compiled against | `libpython3.14.dylib`/`.so`/`.dll` extracted from the `python-build-standalone` archive pinned to `pythonVersion` (`python-checksums.properties`: `macos-aarch64-3.14.7-20260807`, etc.) | n/a — no header exists to disagree with the runtime | read `build.gradle.kts`'s desktop `jvm()` block; no `cinterop`/`headers()` call anywhere in it; `4ed98143:docs/roadmap/ROADMAP.md:1034-1038` records the same conclusion independently | -| iOS / androidNative (Kotlin/Native `cinterop`) | `$targetExtractDir/include/python$libVersion/Python.h` (Android) or the extracted `Python.xcframework/.../Headers` (iOS) — **read from the same extraction the runtime library comes from**, not from any vendored copy | same archive, same extraction, `libVersion`/`targetExtractDir` computed from the one `pythonVersion`/`libVersion` variables | **yes, by construction** — both paths are derived from the same Gradle variable, so they cannot independently drift for a given build | read `python-multiplatform/build.gradle.kts:1093-1128` (`targetIncludePath`, the `cinterops.create("python")` block); confirmed no `defFile`/hardcoded header path overrides it | -| Android JNI (`artMain`, `jni_onload.def`) | none — hand-written `extern` prototypes in `jni_onload.def`, no `#include `, pointers passed as `jlong` | `libpython$libVersion.so` linked with `-lpython$libVersion` from the same extraction tree | n/a for headers; symbol-level agreement already checked by earlier work (`nm`/`objdump` against the shipped `.so`, cited in `docs/roadmap/ROADMAP.md` around the 3.15 migration) | read `python-multiplatform/src/artMain/cinterop/jni_onload.def` and its `build.gradle.kts` cinterop block (no `headers()`/`includeDirs()` call) | -| wasmJs | Emscripten's own CPython 3.14.2 build, entirely separate toolchain (`docs/platforms/wasm-design.md`) | same 3.14.2 build (`Embedded CPython version: 3.14.2`, `docs/archive/wasm-design-experiment-log.md` "Building CPython 3.14.2 for Emscripten"; `runtime : 3.14.2` sample output, `4ed98143:docs/roadmap/ROADMAP.md:1488`) | yes, and deliberately a different minor-patch than the 3.14.7 native default — documented and reasoned about at length in `docs/platforms/wasm-design.md`, not an oversight | +| desktop | none, Panama binds by symbol name at runtime (`java.lang.foreign.Linker`/`SymbolLookup`), no C header is compiled against | `libpython3.14.dylib`/`.so`/`.dll` extracted from the `python-build-standalone` archive pinned to `pythonVersion` (`python-checksums.properties`: `macos-aarch64-3.14.7-20260807`, etc.) | n/a, no header exists to disagree with the runtime | read `build.gradle.kts`'s desktop `jvm()` block; no `cinterop`/`headers()` call anywhere in it; `4ed98143:docs/roadmap/ROADMAP.md:1034-1038` records the same conclusion independently | +| iOS / androidNative (Kotlin/Native `cinterop`) | `$targetExtractDir/include/python$libVersion/Python.h` (Android) or the extracted `Python.xcframework/.../Headers` (iOS), **read from the same extraction the runtime library comes from**, not from any vendored copy | same archive, same extraction, `libVersion`/`targetExtractDir` computed from the one `pythonVersion`/`libVersion` variables | **yes, by construction**, both paths are derived from the same Gradle variable, so they cannot independently drift for a given build | read `python-multiplatform/build.gradle.kts:1093-1128` (`targetIncludePath`, the `cinterops.create("python")` block); confirmed no `defFile`/hardcoded header path overrides it | +| Android JNI (`artMain`, `jni_onload.def`) | none, hand-written `extern` prototypes in `jni_onload.def`, no `#include `, pointers passed as `jlong` | `libpython$libVersion.so` linked with `-lpython$libVersion` from the same extraction tree | n/a for headers; symbol-level agreement already checked by earlier work (`nm`/`objdump` against the shipped `.so`, cited in `docs/roadmap/ROADMAP.md` around the 3.15 migration) | read `python-multiplatform/src/artMain/cinterop/jni_onload.def` and its `build.gradle.kts` cinterop block (no `headers()`/`includeDirs()` call) | +| wasmJs | Emscripten's own CPython 3.14.2 build, entirely separate toolchain (`docs/platforms/wasm-design.md`) | same 3.14.2 build (`Embedded CPython version: 3.14.2`, `docs/archive/wasm-design-experiment-log.md` "Building CPython 3.14.2 for Emscripten"; `runtime : 3.14.2` sample output, `4ed98143:docs/roadmap/ROADMAP.md:1488`) | yes, and deliberately a different minor-patch than the 3.14.7 native default, documented and reasoned about at length in `docs/platforms/wasm-design.md`, not an oversight | **What is genuinely stale, and does not affect any of the above:** two artefacts in this repository still carry CPython 3.13 and are not read by the default build. `python-multiplatform/src/nativeInterop/cinterop/include/patchlevel.h` -is a vendored header tree whose `PY_VERSION` reads `"3.13.0+"` — `git log` shows it landed with +is a vendored header tree whose `PY_VERSION` reads `"3.13.0+"`, `git log` shows it landed with `b184cba5` (pre-dates `pythonVersion` becoming a Gradle property) and `build.gradle.kts` never references `nativeInterop/cinterop/include` at all; cinterop reads headers from the version-keyed -extraction tree instead (table above). `binary/` holds five committed CPython 3.13.0 archives from -`python-build-standalone` release `20241008` (`binary/download.md`); they are consumed only by the -special-cased branch `if (configuredPythonVersion == "3.13.0" && !pythonFreeThreaded)` in the -`desktopJar` task (`build.gradle.kts:1049`), i.e. only if a consumer explicitly builds with +extraction tree instead (table above). `binary/`, which held five unreferenced CPython 3.13.0 archives, was removed on 2026-10-04. The +`-PpythonVersion=3.13.0` branch of the `desktopJar` task reads the vendored tree above, only if a consumer explicitly builds with `-PpythonVersion=3.13.0`. That legacy path has no `Py_LIMITED_API`/header dependency either (desktop never compiles against headers), so even under `-PpythonVersion=3.13.0` there is no header/runtime -split — but `python-checksums.properties` has **no 3.13.0 entries for Android or iOS**, so that flag +split, but `python-checksums.properties` has **no 3.13.0 entries for Android or iOS**, so that flag would fail checksum verification on those two targets rather than silently mixing versions; this was not run to confirm the failure mode, only the absence of pinned checksums. **Correction to the paragraph above, made while verifying it: the 3.13 headers are not inert.** They -are not a *compiler* input — that part holds, `build.gradle.kts` never puts +are not a *compiler* input, that part holds, `build.gradle.kts` never puts `nativeInterop/cinterop/include` on an include path. But two test suites read that directory as their source of truth about the C API: `EmbedApiSurfaceTest.kt:454` and `JniCallConventionClassificationTest.kt:628` both open `src/nativeInterop/cinterop/include` and @@ -291,7 +290,7 @@ call-convention classification. So the one place in this repository where a 3.13 runtime is the evidence layer, not the build: a function whose deprecation or signature changed in 3.14 would be classified from 3.13's text and the tests would agree with themselves. This is the mismatch the check set out to look for, in the one shape nobody had looked at, and it is recorded -rather than fixed here — refreshing the header tree is a separate change with its own verification. +rather than fixed here, refreshing the header tree is a separate change with its own verification. **That separate change was made (2026-08-17), and it answered the open question: nothing this project uses moved between 3.13 and 3.14.** The vendored `include/` tree was replaced with the @@ -321,7 +320,7 @@ VendoredHeaderVersionTest.kt`): it compares `patchlevel.h`'s `PY_MAJOR/MINOR/MIC `gradle.properties`'s `pythonVersion` and fails if they diverge, with a canary test so that a regex that stops matching fails loudly instead of making the comparison vacuous. It was watched failing on the pre-refresh tree (`expected:<3.1[4.7]> but was:<3.1[3.0]>`, sole failure in a -469-test run) before it was trusted. It compares versions, not contents — deliberately, since a +469-test run) before it was trusted. It compares versions, not contents, deliberately, since a content comparison would need the extraction present and so would be unavailable exactly when the extraction has not been run. @@ -333,26 +332,26 @@ and no test; the runtime stdlib comes from the version-keyed extraction instead. them is a repository-size decision, not a correctness one, and is left open. **`Py_LIMITED_API` status, checked directly:** `grep -rn "Py_LIMITED_API"` across `.kts`/`.def` build -files finds it only inside vendored CPython header guards (`#if defined(Py_LIMITED_API) ...`) — it is +files finds it only inside vendored CPython header guards (`#if defined(Py_LIMITED_API) ...`), it is never passed as a compiler define anywhere (no `-DPy_LIMITED_API`, no `compilerOpts` setting it). This independently confirms `4ed98143:docs/roadmap/ROADMAP.md:1031-1038`'s own finding, reached from the free-threading investigation rather than this one: the project does **not** build against the Limited API / Stable ABI in the `Py_LIMITED_API`-macro sense. "abi3" as used elsewhere in this repository's docs means a self-imposed rule about which C API functions this project chooses to call (ones stable across versions), not a `Py_LIMITED_API` compilation mode. So the "Stable ABI permits a header/runtime minor -mismatch" escape hatch the task brief raised does not apply here — but it is not needed, either, +mismatch" escape hatch the task brief raised does not apply here, but it is not needed, either, because the table above shows headers and runtime are never independently sourced in the first place. **Residual risk, marked as such because it was not run:** the table above is a build-configuration argument (headers and runtime derive from one variable), corroborated by *observed* `sys.version`- -equivalent output on desktop and Android and by an existing wasmJs run — not by a from-scratch build +equivalent output on desktop and Android and by an existing wasmJs run, not by a from-scratch build of `compileKotlinIosSimulatorArm64`/`compileKotlinAndroidNativeArm64` performed in this pass (this check reused `docs/roadmap/ROADMAP.md`'s existing, dated build/test evidence rather than re-running Gradle). If a future change hardcodes a header path or a library name outside the `libVersion`/`targetIncludePath` variables audited above, this guarantee would silently stop holding; nothing currently tests for that kind of regression directly (the closest is the version-agnostic assertion in `Python3Test.kt`'s `versionReportsTheConfiguredRelease`, which catches a runtime drift but not a header-only one, since -no target here has a runtime check *of the header's declared version* — the headers have no +no target here has a runtime check *of the header's declared version*, the headers have no executable presence to assert against). **Issues (checked 2026-08-16):** the only open issue against this repository is @@ -367,7 +366,7 @@ Cross-checked against code rather than assumed: - The "335/336/336/315" difference claim was incorrect. Commit `4ac2eca6` proved that `grep` counted the numbers wrong: Kotlin block comments nest, and each file carries superseded pre-migration drafts inside comments, so `grep` scanned them as live code. When counted with comment-aware scanning, - the difference between `expect` and `actual` members is **zero on every target** — it was empty before + the difference between `expect` and `actual` members is **zero on every target**, it was empty before the commit and remained empty after. The miscount pattern (grep unable to see block nesting) has recurred three times in this repository (`EmbedAPI` initial draft, two parser canaries); documenting this as a pattern is valuable to prevent it again. `EmbedAPI.wasmJs.kt` exists too (not one of the @@ -376,7 +375,7 @@ Cross-checked against code rather than assumed: Rather than removing deprecated CPython APIs, the repository kept them and marked them `@Deprecated` in Kotlin: `PyEval_InitThreads` was removed (empty in CPython 3.9+), four functions kept marked `@Deprecated` without `ReplaceWith` (because exact replacements exist but blind swaps would be wrong - in ways the compiler cannot catch — three return borrowed vs. new references, one takes an unnormalized + in ways the compiler cannot catch, three return borrowed vs. new references, one takes an unnormalized triple), and three that CPython marks as "discouraged" (not deprecated) were left alone. Commit `4ac2eca6` adds a test that validates all three targets against a canonical set; the test passes at 466 cases across all five targets. @@ -415,7 +414,7 @@ Cross-checked against code rather than assumed: **Issues (checked 2026-08-16):** two open issues, [`toolchain#2`](https://github.com/thisisthepy/toolchain/issues/2) ("[Todo] Kotlin Gradle Plugin and Build -Tools" — a checklist, sub-issue `pypackpack#2`) and +Tools", a checklist, sub-issue `pypackpack#2`) and [`toolchain#1`](https://github.com/thisisthepy/toolchain/issues/1) ("[Todo] Toolchain-lite for python-only users", parent `pypackpack#2`, one line: `tcl install pythonx-compose`). Full checklist-vs-code table is in §5c. Three corrections to the bullets above, found by reading the current tree (`git log --oneline -12` @@ -424,7 +423,7 @@ runs through `fb1dba7`, `30a064c`, `f7008eb`) rather than by re-deriving them: - **The `createMetaClass.kt` generator this file described above no longer exists.** `git log` shows it deleted in `30a064c` ("Feat: Open the packagePython chain, delegate dependency install, and delete the meta generator"); `find . -iname "createMetaClass*"` in the working tree returns nothing. The "Observed - Current State" bullet above describing it is stale as of that commit — left as written per this + Current State" bullet above describing it is stale as of that commit, left as written per this repository's convention of not erasing superseded claims, corrected here instead. - **"Delete redundant tasks" (this file's own gap item) did not happen by deletion.** `InstallDependenciesTask`, `BuildPythonArtifactTask` and `AssemblePythonPackageTask` all still exist, under the same names. What @@ -435,10 +434,10 @@ runs through `fb1dba7`, `30a064c`, `f7008eb`) rather than by re-deriving them: copy-to-`src/main/assets/python`-then-zip step that lived inline in `PythonPlugin.kt`'s `afterEvaluate` was deleted (that part of the gap item is accurate), but the task classes were rewired, not removed. - **"Wire up DSL" is now partially wrong.** `integration()` and `buildTypes` (`debug`/`release` selection - via `-Ppython.buildType`) are wired as of `fb1dba7` and an earlier commit respectively — confirmed by + via `-Ppython.buildType`) are wired as of `fb1dba7` and an earlier commit respectively, confirmed by reading `PythonPlugin.kt`'s `collectInstallDependencies` (folds `implementations + integrations`) and `resolveActiveBuildType`. `hotReload`, `codePush`, `buildFeatures` (`metaclass`/`compose`), `metaDirs`, - `libDirs` and per-variant `platforms` (e.g. Android min-SDK) remain unwired — but `fb1dba7`'s commit + `libDirs` and per-variant `platforms` (e.g. Android min-SDK) remain unwired, but `fb1dba7`'s commit message states this was tested, not assumed: registering `PythonConfiguration.kt`'s extension was tried and confirmed to compile and do nothing (`usage-example` never imports the DSL package that would reach it), and the rest have no corresponding concept in `pypackpack` to bind to yet. `PythonConfiguration.kt` @@ -475,7 +474,7 @@ runs through `fb1dba7`, `30a064c`, `f7008eb`) rather than by re-deriving them: - **Unverified**: - Wheel patch generation (`WheelPatchBundler.kt`) and incremental upload logic (placeholder files only). -**Issues (checked 2026-08-16):** four open issues — +**Issues (checked 2026-08-16):** four open issues, [`pypackpack#2`](https://github.com/thisisthepy/pypackpack/issues/2) ("[Todo] PyPackPack Initial Development", parent `toolchain#2`, subs `pypackpack#1`/`toolchain#1`), [`pypackpack#1`](https://github.com/thisisthepy/pypackpack/issues/1) ("[Todo] Python Dependency @@ -484,21 +483,21 @@ Management", parent `pypackpack#2`, sub `pypackpack#5`), in uv with Platform Markers", help wanted, a how-to guide rather than a checklist), and [`pypackpack#12`](https://github.com/thisisthepy/pypackpack/issues/12) ("Ambiguous file name due to duplicated name on entirely codebase", a naming-convention proposal). Full checklist-vs-code table in §5c. -Several "Gap to Goal" items above are now stale — this repository moved fast in the last two days -(`git log --oneline -12` runs `488bac0` back through `08353c7`, all dated 2026-08-15/16) — and their commit +Several "Gap to Goal" items above are now stale, this repository moved fast in the last two days +(`git log --oneline -12` runs `488bac0` back through `08353c7`, all dated 2026-08-15/16), and their commit messages are unusually explicit about what they do and do not close, so this agent read each rather than inferring from diff stats: - **"Publish to Maven" is done.** `d10ab76` ("Build: Publish packpack, so toolchain can finally depend on it") added `maven-publish` to `packpack/build.gradle.kts`; the commit message states `publishToMavenLocal` now produces `org.thisisthepy.python.multiplatform:packpack:0.1.0`. The one caveat - the same commit records — CLI dependencies (Clikt, Ktor, zstd) leaking onto a library consumer's - classpath because the CLI lived in the same module — was itself closed one commit later, `488bac0` + the same commit records, CLI dependencies (Clikt, Ktor, zstd) leaking onto a library consumer's + classpath because the CLI lived in the same module, was itself closed one commit later, `488bac0` ("Build: Split the CLI into its own module"): confirmed by `find . -iname BuildCommand.kt`, which now resolves under `cli/src/main/kotlin/.../cli/BuildCommand.kt`, not under `packpack/`. - **"Implement `bundle` stage" is partially done, more precisely than "all four bundlers are placeholders."** `8d7b4b4` ("Feat: Implement the resource bundle...") implemented `ResourceBundler.kt` - (310 lines, its own 328-line test file) — the one bundle type `python-multiplatform` and `toolchain` + (310 lines, its own 328-line test file), the one bundle type `python-multiplatform` and `toolchain` need per the commit message. `BinaryBundler.kt`, `FatWheelBundler.kt`, `SingleWheelBundler.kt` and `WheelPatchBundler.kt` are still placeholders (re-confirmed by this agent, unchanged). But `ResourceBundler` is not yet reachable from the CLI: `grep -n "ResourceBundler\|resource" @@ -509,14 +508,14 @@ inferring from diff stats: Close the three known defects...") added a `registry.properties` file bridging `install`'s project-relative writes and `find`/`list`/`uninstall`'s `~/.pypackpack/python/` reads. `docs/SPEC.md` confirms this in its `python install` section. The same section still lists "Final placement after - download/extraction is incomplete (marked `TODO` in code)" as an open limitation — the registry closes + download/extraction is incomplete (marked `TODO` in code)" as an open limitation, the registry closes the *lookup* mismatch, not this separate placement TODO. - **"Auto-install build tools" is done.** `isMesonInstalled()` used to be a stub returning `true` (per `8d7b4b4`'s commit message); `5de8656` made it probe `meson --version`/`ninja --version` for real and call `Meson.installMeson()` when the probe fails. - **"CLI pass-through flags" is done, not just started.** `5de8656`'s message states `add`/`remove`/ `sync`/`tree` forward unrecognized flags in all three command shapes, verified through the middleware to - the per-target `uv` call — `docs/SPEC.md`'s dependency-management section documents the same allowlist + the per-target `uv` call, `docs/SPEC.md`'s dependency-management section documents the same allowlist (`parsePassthroughArgs` in `cli/CommandExtension.kt`) and a known ordering gotcha (`--target`'s greedy vararg swallows a passthrough flag placed after it). - **Non-Meson compile backends and bundlers remain placeholders, re-confirmed.** `Clang.kt`, `MSVC.kt`, @@ -525,7 +524,7 @@ inferring from diff stats: - **`pypackpack#12`'s file-naming complaint is unresolved.** The suggested rename (`BaseInterface.kt`/`DefaultInterface.kt` duplicated across `dependency/`, `compile/backend/`, `compile/middleware/`, `bundle/`, `deploy/` → `FrontendInterface.kt`/`MiddlewareInterface.kt`/ - `BackendInterface.kt`/`DefaultMiddleware.kt`/`DefaultBackend.kt`) has not been applied — `find . -iname + `BackendInterface.kt`/`DefaultMiddleware.kt`/`DefaultBackend.kt`) has not been applied, `find . -iname "*Interface*.kt"` still returns the ambiguous names the issue complains about, unchanged. - **`pypackpack#5`'s platform-marker guide describes something that already mostly works.** `MarkerPolicy` (`dependency/middleware/DefaultInterface.kt`) builds `platform_system == '...' and platform_machine == @@ -537,12 +536,12 @@ inferring from diff stats: - **`pypackpack#1`'s "Middleware Refactoring" item ("are `DevEnv`/`CrossEnv` redundant wrappers?") is still genuinely open**, not stale: reading `DevEnv.kt` now, its methods still follow "resolve project root → delegate to backend → print success/failure", i.e. still the shape the issue calls a simple - wrapper. `CrossEnv.kt` (552 lines) has grown well past that shape, though — asymmetric, unresolved by + wrapper. `CrossEnv.kt` (552 lines) has grown well past that shape, though, asymmetric, unresolved by this agent's reading, matches the issue's unchecked box. -- **`pypackpack#1`'s "settings.gradle.kts — [ ] uv" item could not be resolved to a concrete claim.** The +- **`pypackpack#1`'s "settings.gradle.kts, [ ] uv" item could not be resolved to a concrete claim.** The issue text is a single unexplained bullet with no elaboration. `settings.gradle.kts` today has no mention of `uv` (checked directly); whether that is the intended scope of the checkbox, this agent could - not determine from the issue alone — left open rather than guessed. + not determine from the issue alone, left open rather than guessed. --- @@ -574,7 +573,7 @@ inferring from diff stats: - Full hot-reload integration with Jupyter notebook server outside `UI.ipynb` static cells. **Issues (checked 2026-08-16):** `gh issue list --repo thisisthepy/pythonx-compose --state open` returns -nothing. There is no issue tracker source for this repository's gap list — everything in "Gap to Goal" +nothing. There is no issue tracker source for this repository's gap list, everything in "Gap to Goal" above is this document's own inference from reading the code, not sourced from an issue. Flagged here so it is not mistaken for the issue-backed items in the other three repos. @@ -590,13 +589,13 @@ it is not mistaken for the issue-backed items in the other three repos. - **Language / Build System / Distribution**: - Pure Python (3.9+). - `setup.py` (package name `toolchain`, version `3.11.0.1`). - - PyPI package / Python CLI tool (`pip install git+https://...`). + - PyPI package / Python CLI tool (`uv tool install git+https://...`). - **Goal in Ecosystem**: - **Legacy reference implementation only.** - Kept for historical reference to understand pre-2024 CPython cross-compilation and recipe management. - Replaced entirely by `pypackpack` (cross-compilation and packaging in Kotlin via `uv`/`crossenv`/`meson`) and `toolchain` (Gradle plugin interface). - **Gap to Goal**: - - Deprecated/superseded. No active development or gap to close — retained for historical reference only. + - Deprecated/superseded. No active development or gap to close, retained for historical reference only. - **Dependency Direction**: - None. Independent legacy Python CLI tool. - **Unverified**: @@ -604,7 +603,7 @@ it is not mistaken for the issue-backed items in the other three repos. --- -## 5b. The Python import surface — decided +## 5b. The Python import surface, decided Four questions were open about what Python code should look like. They are settled. @@ -619,8 +618,8 @@ ordinary jar in that set, so it is covered like anything else. The two producers are therefore split by *what they look at*, not by whether something is reachable: - KSP the consumer's own source — declarations it can see being compiled - artefact walker everything the build resolves — third-party jars, AndroidX included + KSP the consumer's own source, declarations it can see being compiled + artefact walker everything the build resolves, third-party jars, AndroidX included Both run at build time under the same applied plugin. An earlier draft of this file claimed AndroidX could never be reached because KSP does not see it; that conflated one producer's limit @@ -628,27 +627,27 @@ with the system's. What genuinely differs per platform is what a *jar* means. On JVM and Android the artefacts are jars and the walker applies directly. On iOS, androidNative and wasm the artefacts are klibs, and -whether the same walk is possible there — and what a Kotlin declaration from a klib can be bound -to at runtime with no JVM underneath — is the open question, not AndroidX. +whether the same walk is possible there, and what a Kotlin declaration from a klib can be bound +to at runtime with no JVM underneath, is the open question, not AndroidX. -**Update — the walker now exists on one path, and both of those questions have answers.** +**Update, the walker now exists on one path, and both of those questions have answers.** `python-multiplatform-gradle-plugin` gained `generatePythonArtifactBindings`, and -`ksp-fixtures/artifact` carries a resolved `junit:junit:4.13.2` through ASM, a generated +`python-multiplatform-gradle-plugin/fixtures/artifact` carries a resolved `junit:junit:4.13.2` through ASM, a generated `FunctionTableFragment`, `UpcallTable` and `PythonProxySource` to `from junit.runner.Version import id` answering `"4.13.2"`. ROADMAP §16 records it in full; three things in it change what this section says: - **The walker's fragments are a second aggregator (`ArtifactTable`), not additions to KSP's - `FunctionTable`.** One `UpcallTable`, one fragment interface, one Python namespace — but + `FunctionTable`.** One `UpcallTable`, one fragment interface, one Python namespace, but `FunctionTable` keeps meaning "every module in this graph compiled with the processor", which is - what `ksp-fixtures/app` asserts and what a consumer's own table should not silently outgrow. + what `python-multiplatform-ksp/fixtures/app` asserts and what a consumer's own table should not silently outgrow. Install site: `UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments)`. - **ASM alone does not reach `androidx.compose.material3`.** It reaches Java statics and Kotlin `@JvmStatic`s. A Kotlin *top-level* function compiles onto a package-private multi-file part (`kotlin/text/StringsKt__IndentKt`) behind a facade Kotlin cannot name, and its extension receiver is indistinguishable from an ordinary parameter in bytecode. The `@Metadata` *kind* is readable with ASM; the payload that carries the Kotlin names is `d1`/`d2` and needs `kotlin-metadata-jvm`. - So AndroidX is reachable, as this section says — one library short of it, not one design short. + So AndroidX is reachable, as this section says, one library short of it, not one design short. - **klib is the easier half, not the harder one.** `org.jetbrains.kotlin.library.abi.LibraryAbiReader`, already in the `kotlin-compiler-embeddable` this build pins, read `python-multiplatform-iosX64Main.klib` and returned 441 top-level @@ -659,17 +658,17 @@ things in it change what this section says: is an ordinary Kotlin call with no reflection. ~~The open question that remains is not feasibility but cost: `@ExperimentalLibraryAbiReader`, and a ~60 MB compiler artefact on the plugin classpath versioned against the consumer's Kotlin rather than the plugin's.~~ **The cost - question is answered too, and not by shrinking it — by moving it off the plugin's classpath.** + question is answered too, and not by shrinking it, by moving it off the plugin's classpath.** ROADMAP §16e/16f: `KlibScanner` runs inside an isolated Gradle worker (`WorkerExecutor.scanKlibIsolated`) whose classpath is supplied per build rather than baked into the plugin jar, because calling it from the plugin's own classloader throws `NoSuchMethodError` - against a real consumer's `kotlin-util-klib` — a correctness fix that also happens to keep the + against a real consumer's `kotlin-util-klib`, a correctness fix that also happens to keep the ~60 MB artefact off a build that never walks a klib. `PythonArtifactBindingsTask` is wired to it and covered by `KlibScannerTest` against two real klibs. **Dynamic binding is a removed option, not a missing one.** The 2024 design document -(*PyComposeUI*, the open-source contest report) specifies a dynamic binder — Java/Kotlin -Reflection on the JVM target, `ctypes` and `pyobjc` on Kotlin/Native — with a generated meta +(*PyComposeUI*, the open-source contest report) specifies a dynamic binder, Java/Kotlin +Reflection on the JVM target, `ctypes` and `pyobjc` on Kotlin/Native, with a generated meta package existing only so an IDE can infer types against it. That is history. The binder was built statically instead, and the reason is the same one §7 records for choosing a build-time table: a GraalVM native image is a closed world and Kotlin/Native has no reflection, so a name that only @@ -683,18 +682,19 @@ static. **`pythonx.*` is ours.** Whatever we wrap or add lives under that prefix. Kotlin fully-qualified names point at the original; `pythonx` points at our Pythonic layer. The two namespaces do not mix. -*(2026-10-03: "ours" means the ecosystem's — `pythonx` is a real package in pythonx-compose; this -repository's binder creates no `pythonx` module and renames nothing, `AGENTS.md` §12.1–12.2.)* +*(2026-10-03: "ours" means the ecosystem's, `pythonx` is a real package in pythonx-compose; this +repository's binder creates no `pythonx` module and renames no namespace, `AGENTS.md` §12.1–12.2; the +snake_case aliases it serves live on the Kotlin-named modules, SPEC U-12.)* **The wrapping happens in Python, not in Kotlin.** `pythonx.compose.material3` is Python code that uses the generated `androidx.compose.material3` bindings and presents a Pythonic API over them. -There is no hand-written Kotlin wrapper module in between — that is what the existing +There is no hand-written Kotlin wrapper module in between, that is what the existing `io.github.thisisthepy.pycomposeui` `RuntimeKt` and `Runtime_androidKt` are, and they are the shape being moved away from. This settles a question that otherwise looks hard. A `@Composable` function is, after compilation, an ordinary function taking `$composer` and `$changed`; a binding generated from the artefact -exposes that signature as it is, and the Python wrapper passes the composer as a value — which is +exposes that signature as it is, and the Python wrapper passes the composer as a value, which is what `pythonx-compose`'s `Composable` class already does. So nothing on the Kotlin side needs a `@Composable` callable type, and the upcall trampolines do not have to preserve a `@Composable` context. @@ -703,8 +703,8 @@ context. for every Composable is the cost that made the existing `pythonx-compose` 37 files of which 28 are empty. The conversions are rules, not per-function decisions: a Pythonic name maps to the original, the composer and change flags are threaded the same way every time, and a `content=` callable is -adapted the same way every time. So the layer should resolve on demand — a module `__getattr__` -that finds the corresponding binding, adapts it once and caches it — rather than enumerate. +adapted the same way every time. So the layer should resolve on demand, a module `__getattr__` +that finds the corresponding binding, adapts it once and caches it, rather than enumerate. That trades away IDE completion, which is exactly why stubs are generated from the same metadata. *(Corrected 2026-10-03:)* this repository's plugin generates stubs for the Kotlin-named modules, @@ -714,19 +714,19 @@ under Kotlin names (SPEC B-7); the stubs that carry the Pythonic names and signa design that was removed from here). **`JClass`/`JavaClass`, `KClass`/`KotlinClass`, `ObjcClass` work only where the platform has the -thing they name.** No stubs, no substitutes, no forced uniformity across targets — a target that +thing they name.** No stubs, no substitutes, no forced uniformity across targets, a target that has no JVM has no `JClass`, and says so. **`.pyi` generation belongs to the Gradle plugin**, the way PyREPL did it: read the resolved artefacts at build time and emit stubs per source set. It is not a sibling of `PythonProxySource`, which fills `sys.modules` at runtime; these are different products for different consumers, one for -the interpreter and one for the IDE. Long term that generator belongs to `toolchain` — see §5. +the interpreter and one for the IDE. Long term that generator belongs to `toolchain`, see §5. --- ## 5c. Issue checklists against the code -§5 was originally written by reading working copies, and it never opened an issue tracker — this +§5 was originally written by reading working copies, and it never opened an issue tracker, this repository's own `docs/roadmap/ROADMAP.md` mentions the other repos zero times, and the same was true of GitHub issues. That is the gap this section closes. Issues read in full, 2026-08-16: @@ -738,64 +738,64 @@ That is the gap this section closes. Issues read in full, 2026-08-16: `pythonx-compose` has none open. Seven issues came back across the other three repos, each read with `gh issue view --repo `, then checked against the working copy (not against the issue's own description of itself). "✅ done" below means this agent found the corresponding code and, where the issue -or a commit message made a narrower claim, checked that narrower claim too — not just that a file with a +or a commit message made a narrower claim, checked that narrower claim too, not just that a file with a plausible name exists. -### `toolchain#2` — "[Todo] Kotlin Gradle Plugin and Build Tools" +### `toolchain#2`, "[Todo] Kotlin Gradle Plugin and Build Tools" | Checklist item | Issue's checkbox | Actual state (checked against code) | |---|---|---| -| Create the basic structure of the plugin | ☑ checked | ✅ matches — `PythonPlugin.kt` registers the `python` extension and three tasks | -| Python version setup → Version Enum (alpha, rc, normal) | ☐ unchecked | ❌ matches — `compileSdk` is a plain `String` in `DSLCore.kt`; no enum anywhere | -| Build target platform setup → platform-specific min-SDK setting | ☐ unchecked | 🟡 partial — `AndroidPlatformExtension.androidSdk: Int` exists in `DSLPlatforms.kt` but no task reads it (per `fb1dba7`'s own commit message: "platforms has a partial hook... mapping them silently would be wrong") | -| Build target platform setup → check Kotlin-side enabled build target | ☐ unchecked | ❌ matches — no code found that inspects which KMP targets are enabled | -| Hot reload / Code Push → expose a direct run button | ☐ unchecked | ❌ matches — `HotReloadExtension`/`CodePushExtension` (`DSLPackaging.kt`) are pure data holders; `fb1dba7`: "hotReload and codePush have no backend concept in packpack" | -| SourceSet setup → `implementation` | ☐ unchecked | ✅ **stale checkbox** — wired since before `fb1dba7`; `collectInstallDependencies` reads `implementations` | -| SourceSet setup → `integration` | ☐ unchecked | ✅ **stale checkbox** — `fb1dba7` ("Feat: Make integration() install...") folds `integrations` into the same install list; it is treated identically to `implementation`, no `KLIBDEPENS` distinction (deliberately, per the same commit — nothing in `pypackpack` has that concept) | -| SourceSet setup → etcs (src/resource path, sourceset naming) | ☐ unchecked | ❌ matches — `SourceSetConfig.srcDirs`/`metaDirs`/`libDirs` (`DSLBuild.kt`) accept values via DSL but nothing reads them (`fb1dba7`: "metaDirs and libDirs have nothing in the resource bundler to bind to") | -| CompileLevel setup (debug/release, project flavors) | ☐ unchecked | 🟡 partial — `debug`/`release` selection via `resolveActiveBuildType`/`-Ppython.buildType` is wired; project flavors (`buildFeatures`) are not (`fb1dba7`: "buildFeatures has no consumer at all") | -| Automate the build process and integrate with KMP | ☐ unchecked | 🟡 largely done, not fully end-to-end verified by this agent — `buildTask`→`installTask`→`packageTask` chain runs and delegates real work to `pypackpack`'s `ResourceBundler` (`30a064c`); this agent did not run `usage-example` to confirm a full build | - -### `toolchain#1` — "[Todo] Toolchain-lite for python-only users" - -One line, no checklist: `tcl install pythonx-compose`. ❌ Not started — `find . -iname "*tcl*" -o -iname +| Create the basic structure of the plugin | ☑ checked | ✅ matches, `PythonPlugin.kt` registers the `python` extension and three tasks | +| Python version setup → Version Enum (alpha, rc, normal) | ☐ unchecked | ❌ matches, `compileSdk` is a plain `String` in `DSLCore.kt`; no enum anywhere | +| Build target platform setup → platform-specific min-SDK setting | ☐ unchecked | 🟡 partial, `AndroidPlatformExtension.androidSdk: Int` exists in `DSLPlatforms.kt` but no task reads it (per `fb1dba7`'s own commit message: "platforms has a partial hook... mapping them silently would be wrong") | +| Build target platform setup → check Kotlin-side enabled build target | ☐ unchecked | ❌ matches, no code found that inspects which KMP targets are enabled | +| Hot reload / Code Push → expose a direct run button | ☐ unchecked | ❌ matches, `HotReloadExtension`/`CodePushExtension` (`DSLPackaging.kt`) are pure data holders; `fb1dba7`: "hotReload and codePush have no backend concept in packpack" | +| SourceSet setup → `implementation` | ☐ unchecked | ✅ **stale checkbox**, wired since before `fb1dba7`; `collectInstallDependencies` reads `implementations` | +| SourceSet setup → `integration` | ☐ unchecked | ✅ **stale checkbox**, `fb1dba7` ("Feat: Make integration() install...") folds `integrations` into the same install list; it is treated identically to `implementation`, no `KLIBDEPENS` distinction (deliberately, per the same commit, nothing in `pypackpack` has that concept) | +| SourceSet setup → etcs (src/resource path, sourceset naming) | ☐ unchecked | ❌ matches, `SourceSetConfig.srcDirs`/`metaDirs`/`libDirs` (`DSLBuild.kt`) accept values via DSL but nothing reads them (`fb1dba7`: "metaDirs and libDirs have nothing in the resource bundler to bind to") | +| CompileLevel setup (debug/release, project flavors) | ☐ unchecked | 🟡 partial, `debug`/`release` selection via `resolveActiveBuildType`/`-Ppython.buildType` is wired; project flavors (`buildFeatures`) are not (`fb1dba7`: "buildFeatures has no consumer at all") | +| Automate the build process and integrate with KMP | ☐ unchecked | 🟡 largely done, not fully end-to-end verified by this agent, `buildTask`→`installTask`→`packageTask` chain runs and delegates real work to `pypackpack`'s `ResourceBundler` (`30a064c`); this agent did not run `usage-example` to confirm a full build | + +### `toolchain#1`, "[Todo] Toolchain-lite for python-only users" + +One line, no checklist: `tcl install pythonx-compose`. ❌ Not started, `find . -iname "*tcl*" -o -iname "*lite*"` in the `toolchain` working copy (excluding `.git`/`build`) returns nothing. -### `pypackpack#2` — "[Todo] PyPackPack Initial Development" +### `pypackpack#2`, "[Todo] PyPackPack Initial Development" | Checklist item | Issue's checkbox | Actual state (checked against code) | |---|---|---| -| Create the basic structure of the plugin | ☐ unchecked | (parent bullet, not independently checkable — see sub-items) | -| Python version setup → Version Enum | ☐ unchecked | ❌ same as `toolchain#2` — no enum found in either repo | +| Create the basic structure of the plugin | ☐ unchecked | (parent bullet, not independently checkable, see sub-items) | +| Python version setup → Version Enum | ☐ unchecked | ❌ same as `toolchain#2`, no enum found in either repo | | Build target platform setup → platform-specific min-SDK | ☐ unchecked | ❌ not found in `pypackpack` | -| Build target platform setup → check Kotlin-side enabled build target | ☑ checked | 🟡 not independently verified by this agent — no corroborating code found in the time available; recorded as unverified rather than disputed | +| Build target platform setup → check Kotlin-side enabled build target | ☑ checked | 🟡 not independently verified by this agent, no corroborating code found in the time available; recorded as unverified rather than disputed | | Hot reload / Code Push → run button | ☐ unchecked | ❌ matches | -| SourceSet setup → implementation (python-only) | ☐ unchecked | 🟡 the underlying dependency-add path exists (`DependencyBackend.addDependencies`), but this is `toolchain`'s DSL concept, not `pypackpack`'s — checkbox sits on the wrong side of the boundary the ecosystem doc draws in §1 | -| SourceSet setup → integration (python+kotlin mixed) | ☐ unchecked | ❌ matches — no `KLIBDEPENS` handling anywhere in `pypackpack` (grepped, zero hits) | -| External tool detection → Nuitka | ☐ unchecked | ❌ matches — `Nuitka.kt` is 4 lines | -| External tool detection → Host Python | ☑ checked | ✅ matches — `python install` downloads a prebuilt CPython from `python-multiplatform`'s GitHub releases (`docs/SPEC.md`, confirmed) | -| External tool detection → MSVC, Clang | ☐ unchecked | ❌ matches — both 4-line placeholders | -| External tool detection → Poetry or UV | ☑ checked | ✅ matches — `dependency/backend/external/UV.kt` is a real, exercised integration | -| External tool detection → Crossenv or equivalent | ☐ unchecked | 🟡 partial — `CrossEnv.kt` exists at 552 lines and is exercised by `add`/`sync`/`tree` per-target commands, but "creating a dedicated venv per target" is explicitly listed as not-yet-implemented in `docs/SPEC.md` | -| Compilation modes → instant (pure python) | ☑ checked | 🟡 not verified as a selectable mode — `docs/SPEC.md` states the `--level` CLI flag is "accepted but ignored" for `build`, i.e. there's exactly one behavior today, which happens to look like `instant`, not a selection mechanism | -| Compilation modes → bytecode/mixed/native | ☐ unchecked | ❌ matches — `docs/SPEC.md` lists all three as "Not yet implemented (target)" | +| SourceSet setup → implementation (python-only) | ☐ unchecked | 🟡 the underlying dependency-add path exists (`DependencyBackend.addDependencies`), but this is `toolchain`'s DSL concept, not `pypackpack`'s, checkbox sits on the wrong side of the boundary the ecosystem doc draws in §1 | +| SourceSet setup → integration (python+kotlin mixed) | ☐ unchecked | ❌ matches, no `KLIBDEPENS` handling anywhere in `pypackpack` (grepped, zero hits) | +| External tool detection → Nuitka | ☐ unchecked | ❌ matches, `Nuitka.kt` is 4 lines | +| External tool detection → Host Python | ☑ checked | ✅ matches, `python install` downloads a prebuilt CPython from `python-multiplatform`'s GitHub releases (`docs/SPEC.md`, confirmed) | +| External tool detection → MSVC, Clang | ☐ unchecked | ❌ matches, both 4-line placeholders | +| External tool detection → Poetry or UV | ☑ checked | ✅ matches, `dependency/backend/external/UV.kt` is a real, exercised integration | +| External tool detection → Crossenv or equivalent | ☐ unchecked | 🟡 partial, `CrossEnv.kt` exists at 552 lines and is exercised by `add`/`sync`/`tree` per-target commands, but "creating a dedicated venv per target" is explicitly listed as not-yet-implemented in `docs/SPEC.md` | +| Compilation modes → instant (pure python) | ☑ checked | 🟡 not verified as a selectable mode, `docs/SPEC.md` states the `--level` CLI flag is "accepted but ignored" for `build`, i.e. there's exactly one behavior today, which happens to look like `instant`, not a selection mechanism | +| Compilation modes → bytecode/mixed/native | ☐ unchecked | ❌ matches, `docs/SPEC.md` lists all three as "Not yet implemented (target)" | | Build Tools → configure Nuitka | ☐ unchecked | ❌ matches | | Build Tools → compatibility testing with Android | ☐ unchecked | ❌ not found | -| Build Tools → minification | ☐ unchecked | ❌ matches — `compile/middleware/minification/` has only the base interface | +| Build Tools → minification | ☐ unchecked | ❌ matches, `compile/middleware/minification/` has only the base interface | -### `pypackpack#1` — "[Todo] Python Dependency Management" +### `pypackpack#1`, "[Todo] Python Dependency Management" | Checklist item | Issue's checkbox | Actual state (checked against code) | |---|---|---| -| Handle `pyproject.toml` → lossless modification | ☑ checked | ✅ matches — `utils/toml/TomlEditor.kt` + `TomlValue.kt` exist and are exercised (SPEC.md's `target add/remove`, `package remove` sections describe editing specific keys in place) | -| Fix `CrossEnv.kt` abstraction → encapsulate UV logic in backend | ☑ checked | ✅ plausible, not fully re-derived — `CrossEnv.kt`'s target-marker logic (`MarkerPolicy`) and per-target `uv` calls live in the middleware/backend layers this item describes; this agent did not diff against a pre-fix version to confirm the decoupling directly | -| Middleware Refactoring → is `DevEnv`/`CrossEnv` still a redundant wrapper? | ☐ unchecked | ✅ matches, genuinely still open — `DevEnv.kt` (240 lines) still follows "resolve project root → delegate to backend → print result" for every method, the shape the issue questions. `CrossEnv.kt` (552 lines) has grown well past a simple wrapper, which is itself an argument the two are no longer symmetric — unresolved either way | -| Target Platforms → research a cleaner architecture for platform-specific deps via UV | ☐ unchecked | 🟡 partial — `MarkerPolicy`/`Platforms.describeTarget` (`utils/Platforms.kt`) already implement a `platform_system`/`platform_machine` marker scheme and thread `--python-platform` through `tree`/`sync`, i.e. a pattern exists; whether it counts as the "research" this item asks for is a judgment call this agent did not make either way | -| `settings.gradle.kts` → `uv` | ☐ unchecked | ❓ could not resolve — the issue gives no elaboration beyond the single word "uv"; `settings.gradle.kts` today has no `uv` reference at all, but this agent could not determine what behavior the checkbox is asking for, so this is left open rather than guessed | +| Handle `pyproject.toml` → lossless modification | ☑ checked | ✅ matches, `utils/toml/TomlEditor.kt` + `TomlValue.kt` exist and are exercised (SPEC.md's `target add/remove`, `package remove` sections describe editing specific keys in place) | +| Fix `CrossEnv.kt` abstraction → encapsulate UV logic in backend | ☑ checked | ✅ plausible, not fully re-derived, `CrossEnv.kt`'s target-marker logic (`MarkerPolicy`) and per-target `uv` calls live in the middleware/backend layers this item describes; this agent did not diff against a pre-fix version to confirm the decoupling directly | +| Middleware Refactoring → is `DevEnv`/`CrossEnv` still a redundant wrapper? | ☐ unchecked | ✅ matches, genuinely still open, `DevEnv.kt` (240 lines) still follows "resolve project root → delegate to backend → print result" for every method, the shape the issue questions. `CrossEnv.kt` (552 lines) has grown well past a simple wrapper, which is itself an argument the two are no longer symmetric, unresolved either way | +| Target Platforms → research a cleaner architecture for platform-specific deps via UV | ☐ unchecked | 🟡 partial, `MarkerPolicy`/`Platforms.describeTarget` (`utils/Platforms.kt`) already implement a `platform_system`/`platform_machine` marker scheme and thread `--python-platform` through `tree`/`sync`, i.e. a pattern exists; whether it counts as the "research" this item asks for is a judgment call this agent did not make either way | +| `settings.gradle.kts` → `uv` | ☐ unchecked | ❓ could not resolve, the issue gives no elaboration beyond the single word "uv"; `settings.gradle.kts` today has no `uv` reference at all, but this agent could not determine what behavior the checkbox is asking for, so this is left open rather than guessed | -### `pypackpack#5` — "Managing Multi-Platform Dependencies in uv with Platform Markers" (help wanted, guide not checklist) +### `pypackpack#5`, "Managing Multi-Platform Dependencies in uv with Platform Markers" (help wanted, guide not checklist) -Not a checklist — a how-to document proposing `platform_system`/`platform_machine` PEP 508 markers per +Not a checklist, a how-to document proposing `platform_system`/`platform_machine` PEP 508 markers per target. 🟡 **largely already matches the shipped design**: `MarkerPolicy.markerForTarget` in `dependency/middleware/DefaultInterface.kt` builds exactly `platform_system == '...' and platform_machine == '...'`, and `--python-platform` is passed to `uv tree`/`uv add` per target. The one place code and guide @@ -804,7 +804,7 @@ come back as `platform_machine`/`sys_platform` rather than `platform_system`/`pl breaks `remove --target`'s re-derived-marker matching for some dependencies. So the guide's approach is implemented, with one recorded, unfixed bug in marker-round-tripping. -### `pypackpack#12` — "Ambiguous file name due to duplicated name on entirely codebase" (question, not checklist) +### `pypackpack#12`, "Ambiguous file name due to duplicated name on entirely codebase" (question, not checklist) ❌ **Not addressed.** The issue proposes renaming `BaseInterface.kt`/`DefaultInterface.kt` (duplicated across `dependency/`, `compile/backend/`, `compile/middleware/`, `bundle/`, `deploy/`) to @@ -812,7 +812,7 @@ across `dependency/`, `compile/backend/`, `compile/middleware/`, `bundle/`, `dep `DefaultBackend.kt`. `find . -iname "*Interface*.kt"` in the current tree still shows `BaseInterface.kt` and `DefaultInterface.kt` repeated in all five domains named in the issue, unchanged. -### `python-multiplatform#4` — "Add Python/C API expect declaration reclassification and actual definitions" +### `python-multiplatform#4`, "Add Python/C API expect declaration reclassification and actual definitions" No checklist, two prose tasks. ✅ **Closed** by commit `4ac2eca6` ("Fix: Remove the drafts stranded in comments, and reclassify what CPython deprecated"). The "20-member gap" this document had reported @@ -834,13 +834,13 @@ each repository's block). Two kinds of mismatch, kept separate rather than merge `pypackpack#1`'s `DevEnv`/`CrossEnv` redundancy question and its unexplained `settings.gradle.kts`/`uv` item; `toolchain#2`'s Python-version-enum and Kotlin-target-detection sub-items; `pypackpack#2`'s compile-mode checklist (`instant`/`bytecode`/`mixed`/`native`) and Android-build-compatibility item. - These were not in this document before this pass — they are what "renders §5 incomplete" in the sense + These were not in this document before this pass, they are what "renders §5 incomplete" in the sense the task described, now folded into each repository's "Issues" note above rather than duplicated again here. - **In §5's original gap lists, absent from any issue**: `PythonMultiplatform`'s `@Composable`-callable-shape - gap (already closed per §4 item 1, and never was a `python-multiplatform` issue — it is `pythonx-compose`'s + gap (already closed per §4 item 1, and never was a `python-multiplatform` issue, it is `pythonx-compose`'s problem surfacing in this repo's code), its `sys.meta_path` lazy-import gap, and all of `pythonx-compose`'s - "Gap to Goal" list. These are this document's own judgment calls, not sourced from a tracker — flagged in + "Gap to Goal" list. These are this document's own judgment calls, not sourced from a tracker, flagged in the `pythonx-compose` "Issues" note above since that repository has zero open issues to source anything from. @@ -851,3 +851,24 @@ each repository's block). Two kinds of mismatch, kept separate rather than merge It describes five repositories and sits in one of them, because that is the only one with an active working copy and a documentation habit. If the organisation grows a place for cross-repo documents, this belongs there, and what stays here is the last section's first entry. + +## 7. Priorities and the pythonx family (decided 2026-10-04) + +The maintainer focuses on darkpyonix for a while; thisisthepy proceeds **linearly** in this order: +torchnative, then python-multiplatform (TypedPython included), then pypackpack and toolchain-lite, +then pythonx-compose, then the other pythonx packages and Gemstone. Each repository first records its +decisions as issues, finishes document updates and folder cleanup, and lists what is left until its +next release; lower-priority repositories then hold new heavy work until their turn. + +| Package | Decision | Where it is recorded | +|---|---|---| +| `pythonx-concurrent` | Structured concurrency bridging asyncio and Kotlin coroutines. Spec written in detail, implementation deferred. Python coroutines may run on JVM virtual threads and on any Kotlin dispatcher by being stepped from Kotlin ("Kotlin mode"); blocking-style Python on virtual threads is not offered. | its `docs/SPEC.md` (§7); binder requests #164, #161, #162 | +| `pythonx-platform` | Device features from Python (plyer-like): camera, sensors, notifications, file picker, permissions. The Kotlin implementations belong in `compose-multiplatform-core-extended` as multiplatform `androidx.*` libraries; Python reaches them through the binder's names (snake_case aliases included); `pythonx-platform` is at most a thin Pythonic layer. Repository and spec to be created in its turn. | OS notifications: compose-multiplatform-core-extended#13 | +| `pythonx-game` | Direction open. Recommended, not measured: Filament as the 3D renderer (scene level, the height of three.js, PBR and glTF, Vulkan/Metal/GL, Android Kotlin API, Apache-2.0) with a Python declarative layer on top; Compose's Skia canvas for 2D; wgpu only where custom GPU compute is needed (its per-call validation costs CPU, and it is a GPU API, not a scene engine). | this section | +| `pythonx-native` | Not a repository. Building Rust crates as Python extension modules for the app is pypackpack's Cargo backend; Kotlin reaches the crate through python-multiplatform. | pypackpack#65 | + +TypedPython decisions of the same day: GC collection timing is outside the CPython-equivalence +guarantee (option (b), SPEC N-12); the gap-closing items of the Cython/numba/LPython comparison are +approved (#147 to #151, #143); open decisions are #152 (yielding the GIL in array loops), #153 +(fastmath opt-in) and whether the first usable release may compile on desktop only. Free-threaded +readiness across repositories is #158 (GIL stays the default until decided), with #159 to #163. diff --git a/docs/design/kotlin-extensions-in-python.md b/docs/design/kotlin-extensions-in-python.md index fe2da0d67..33cb62f61 100644 --- a/docs/design/kotlin-extensions-in-python.md +++ b/docs/design/kotlin-extensions-in-python.md @@ -2,14 +2,14 @@ Compose is the first library whose API this binder has to reach that is *mostly* extension functions over value-class parameters. `Modifier.padding(16.dp)` is the shape, and neither half of -it — the extension receiver nor the `Dp` — survives into JVM bytecode as itself. +it, the extension receiver nor the `Dp`, survives into JVM bytecode as itself. This file records what the Compose artefacts actually contain, what the artefact walker binds from them today, and what the Python surface should look like. Everything under "Measured" was counted from real jars on 2026-08-14 and the commands are reproducible from §1. Everything under a "Judged" heading is a decision, and the alternatives that were rejected are named. -## Status — what this document has been overtaken by, and where +## Status, what this document has been overtaken by, and where This file was written at commit `f378ce87` (2026-08-14 15:28). Two commits landed within the hour that invalidated parts of it. Nothing measured here has been deleted; each superseded claim is left @@ -26,7 +26,7 @@ in place with what replaced it, because the old measurement is why the replaceme | §1, the Compose version | 1.6.11 | the measurements here were taken on 1.6.11 and are kept as such; the build now pins Compose Multiplatform **1.11.1** (`gradle/libs.versions.toml`, `compose-plugin`), with material3 1.9.0 | | §3.2, 43 `Modifier` extensions declined for a function-typed parameter | "a real boundary limit" | function-typed slots bind now (SPEC B-5); `ArtifactScanner`'s KDoc records **157 `Modifier` extensions bound and 8 declined** after `functionSlotOrNull` (not re-measured for this revision) | | §3.1/§6, where the overload dispatcher lives | "in `pythonx`" | in **this repository's** binding layer, `python_multiplatform.binding` (`_Overloads` in `PythonxAdapter.kt`), which serves the base name (`padding`) and selects among the `name__` entries (SPEC U-8) | -| §4.1, primitive receivers as `pythonx.compose.ui.unit.to_dp(x)` | snake_case, under `pythonx` | the binder serves Kotlin names only (`docs/INTENT.md` §2.2): such an extension is a module-level function on its Kotlin-named module, receiver first. A Pythonic spelling is pythonx-compose's | +| §4.1, primitive receivers as `pythonx.compose.ui.unit.to_dp(x)` | under `pythonx` | the binder renames no namespace (`docs/INTENT.md` §2.2): such an extension is a module-level function on its Kotlin-named module, receiver first, reachable as `toDp` and as its Pythonic alias `to_dp` (SPEC U-12) | | §4.4, the allowlist | decided, not built | built at run time in the binding layer: `_VALUE_CLASS_ALLOWLIST` starts **empty** and pythonx-compose fills it through `allow_raw_primitive` (`PythonxAdapter.kt`) | | §4.5, `.pyi` with `@overload` and `Dp \| float` | proposed for this plugin | the plugin emits Kotlin-named stubs with boundary types only (SPEC B-7, `docs/design/pyi-generation-design.md`); the Pythonic stub is pythonx-compose's. §4.5 is replaced below; the original is in [`docs/archive/kotlin-extensions-in-python-pyi.md`](../archive/kotlin-extensions-in-python-pyi.md) | | §4.6, composables | "remain blocked" | callable from Python inside a composition (SPEC B-6); see §4.6's update | @@ -63,13 +63,13 @@ jars. Sanity check that the harness works: the same binary returns 9 entries fro **§3.2's re-measurement does not use this harness.** A standalone copy has to be re-copied every time the walker changes, and it was already stale by the time §3's zero was recorded. The replacement is `ArtifactScannerTest.composeModifierExtensionsSurviveBothGates`, which runs *the* `ArtifactScanner` -against the same cached jars on every build — no copy, and no way for the number to go unrecounted. +against the same cached jars on every build, no copy, and no way for the number to go unrecounted. It walks 6 of the 19 jars rather than all of them; §3.2 says which, and why the two counts are not comparable. --- -## 2. Measured — the size of the surface +## 2. Measured, the size of the surface ### 2.1 Overall @@ -90,9 +90,9 @@ comparable. | … public | 251 | 252 | | … internal | 40 | 37 | | … private | 39 | 32 | -| public **top-level** (the chainable API) | **177** | — | -| … mangled | **52 (29.4%)** | — | -| public **member** extensions (need a dispatch receiver) | 74, over 32 owner classes | — | +| public **top-level** (the chainable API) | **177** | - | +| … mangled | **52 (29.4%)** | - | +| public **member** extensions (need a dispatch receiver) | 74, over 32 owner classes | - | The two ecosystems agree to within one function, so the numbers are a property of Compose and not of one packaging of it. @@ -125,7 +125,7 @@ Metadata arity excludes the synthetic parameters; the JVM descriptor does not. low.** `docs/archive/pythonx-adapter-design.md` §5.2 found this by re-counting with `javap -p`; the count has since been reproduced a third time, independently, against the same 1.6.11 jars. What was missed is **`$default`**. The trailing JVM parameters of `Button` are `Composer, int, int` and of -`Text-fLXpl1I` are `Composer, int, int, int` — `$composer`, one or two `$changed` masks, and a +`Text-fLXpl1I` are `Composer, int, int, int`, `$composer`, one or two `$changed` masks, and a `$default` mask. `$default` is easy to miss because Compose does not carry it the way the rest of Kotlin does. @@ -136,8 +136,8 @@ itself instead. Reproduce with: unzip -o -q "$GRADLE_CACHE/.../material3-desktop-1.6.11.jar" 'androidx/compose/material3/ButtonKt.class' javap -p androidx/compose/material3/ButtonKt.class -The conclusion drawn from this table — *never derive arity from a descriptor; metadata arity is the -source* (§4.6) — is unaffected, and the correction strengthens it: the error was made by counting +The conclusion drawn from this table, *never derive arity from a descriptor; metadata arity is the +source* (§4.6), is unaffected, and the correction strengthens it: the error was made by counting descriptors by hand, which is the exact failure the rule forbids a machine to make. The `+2/+3/+4/+5` distribution in §2.6 was **not** re-derived and may be shifted the same way; treat it as unconfirmed. @@ -145,15 +145,15 @@ distribution in §2.6 was **not** re-derived and may be shifted the same way; tr `Button`, `Card`, `Column`, `Row` are not mangled because none of their parameters is. **`Text--4IGK_g` appears twice in the same class with different descriptors.** That is not an -anomaly — across corpus A, **108 mangled JVM names are ambiguous within their own class**. The +anomaly, across corpus A, **108 mangled JVM names are ambiguous within their own class**. The mangling suffix is a hash of the *value-class parameter signature only*, so it is shared by unrelated functions: `-3ABfNKs` is worn by `padding`, `size`, `width`, `height`, `requiredSize`, `requiredWidth`, `requiredHeight`, `times` and `surfaceColorAtElevation`, all of which take a single `Dp`; `-VpY3zN4` is worn by thirteen functions taking two `Dp`s. This retires an approach rather than complicating it. Any binder that *looks a function up by -name* — Java reflection, a Python `__dict__` sweep, the 2024 `pythonx-compose` probe that scanned -`ButtonKt.__dict__` for keys beginning `"Button-"` — cannot address a Compose function +name*, Java reflection, a Python `__dict__` sweep, the 2024 `pythonx-compose` probe that scanned +`ButtonKt.__dict__` for keys beginning `"Button-"`, cannot address a Compose function unambiguously even in principle. This binder does not look functions up by name: it emits Kotlin source that says `Modifier.padding(16.dp)` and lets `kotlinc` do the mangling. The mangled name never appears in anything we generate. @@ -187,7 +187,7 @@ under raw-number coercion, is wrong twice over. `16.sp` produces a `TextUnit`, a The high word is the unit tag (`1` = Sp, `2` = Em, `0` = Unspecified) and the low word is the raw float bits. A raw Python `16` handed to a `TextUnit` parameter therefore has tag `0` and decodes -as `TextUnit.Unspecified` — not "16 of the wrong unit", but *silently no value at all*. +as `TextUnit.Unspecified`, not "16 of the wrong unit", but *silently no value at all*. Only **three** value classes in all of Compose wrap a `Float`: `Dp`, `text.style.BaselineShift`, and `LineHeightStyle.Alignment`. Ambiguity between float-shaped units is a non-problem; packing is @@ -208,12 +208,12 @@ occurrence: `Dp` 56, `Role` 11, `Color` 8, `TextUnit` 4, `TransformOrigin` 4, `D `BlurredEdgeTreatment` 2, `MarqueeAnimationMode` 1, `CompositingStrategy` 1, `PointerEventType` 1. **`Dp` alone is 62% of all value-class parameter occurrences on `Modifier`.** -### 2.5 Name collisions — far rarer than assumed, and the first count was wrong +### 2.5 Name collisions, far rarer than assumed, and the first count was wrong Counted naively over *every* public extension function, 41 `(receiver, name)` pairs appear in more than one package, which would have made a "require qualification on collision" rule -unaffordable. That count is an artefact. Most of it is **member** extension functions — `Dp.toPx` -is declared inside the `Density` interface, `Modifier.align` inside `RowScope` — which metadata +unaffordable. That count is an artefact. Most of it is **member** extension functions, `Dp.toPx` +is declared inside the `Density` interface, `Modifier.align` inside `RowScope`, which metadata records again in every implementing class. They are not competing top-level declarations and Kotlin's import scope never sees them as such. @@ -243,7 +243,7 @@ costs five names in the whole library. descriptor; matching on the substring `Composable` inflates this to 524 by catching `ComposableTarget` and friends). 183 are mangled. -Every one of them gains synthetic JVM parameters — `$composer`, one or two `$changed` bitmasks, +Every one of them gains synthetic JVM parameters, `$composer`, one or two `$changed` bitmasks, zero to two `$default` bitmasks: +2 params : 80 +3 params : 349 +4 params : 62 +5 params : 9 @@ -261,12 +261,12 @@ disjoint problems. --- -## 3. Measured — what the artefact walker bound from Compose, and what it binds now +## 3. Measured, what the artefact walker bound from Compose, and what it binds now **This section is kept as it was measured, because the zero is why the work that follows it happened.** It describes commit `f378ce87` (2026-08-14 15:28). Two gates are named in the funnel below and a third in §3.1; all three are gone, and §3.2 is the re-measurement. Read §3 and §3.1 as -the diagnosis and §3.2 as the outcome — not as a description of the walker today. +the diagnosis and §3.2 as the outcome, not as a description of the walker today. **Zero. From all 19 jars of corpus A, and all 19 of corpus B.** @@ -291,12 +291,12 @@ Two independent gates each zero Compose on their own. **The metadata-kind gate.** A Kotlin top-level function compiles into a file facade (`k=2`) or a multi-file part (`k=5`), and the walker declines both because their JVM names are not names Kotlin -can write. That drops **all 1,411 public top-level Compose functions**, mangled or not — +can write. That drops **all 1,411 public top-level Compose functions**, mangled or not, `ButtonKt.Button` and `PaddingKt.padding` alike. The entire top-level Compose API is behind this one gate. -> **Closed by `15fc5a62`, 25 minutes after this was written.** The gate was one line — -> `kotlinMetadataKind(node)?.let { if (it != KOTLIN_KIND_CLASS) return emptyList() }` — and it is +> **Closed by `15fc5a62`, 25 minutes after this was written.** The gate was one line, +> `kotlinMetadataKind(node)?.let { if (it != KOTLIN_KIND_CLASS) return emptyList() }`, and it is > gone: `ArtifactScanner.scanClassNode`'s `when` now dispatches on the decoded > `KotlinClassMetadata`, with arms for `FileFacade` (`k=2`) and `MultiFileClassFacade` (`k=4`, > which fetches each part by name and reads the part's own `kmPackage`). The `KOTLIN_KIND_CLASS` @@ -312,11 +312,11 @@ something that is not a primitive, `String`, `byte[]` or `void`. > `python.multiplatform.reflection.TypeTag.OBJECT` **already existed**, and so did the entire > runtime path behind it: `UpcallTrampoline.marshalResult` already put a returned Kotlin object into > `HandleTable` and handed Python the integer, and `toKotlinObject` already resolved it back. All of -> that predates this document — it is checkable in the tree at `f378ce87`. What the *walker* lacked +> that predates this document, it is checkable in the tree at `f378ce87`. What the *walker* lacked > was not a marshalling category but a **Kotlin type name to cast the handle to**, which a JVM > descriptor genuinely cannot supply (`Ljava/util/List;` has no Kotlin spelling Kotlin will accept -> written out). `@Metadata`'s classifier *is* that name — `androidx/compose/ui/Modifier`, `/` for -> `.` and nothing else to guess — so the object case lives in `resolveKotlinType` +> written out). `@Metadata`'s classifier *is* that name, `androidx/compose/ui/Modifier`, `/` for +> `.` and nothing else to guess, so the object case lives in `resolveKotlinType` > (`KotlinMetadata.kt`) and the descriptor-only `boundaryTypeOf` still has no `OBJECT` arm, on > purpose. The gate was real; "no boundary type exists" was not the reason it was shut. @@ -324,7 +324,7 @@ something that is not a primitive, `String`, `byte[]` or `void`. it, but the 190 it lets through die at the next gate anyway. This matters for how the gap is described: the mangling filter is being replaced right now by metadata-driven understanding, and when it is gone the Compose number will still be zero until the facade gate and the type gate move -too. The 52-of-177 mangling figure in §2.2 is therefore not a measure of what is unreachable — it +too. The 52-of-177 mangling figure in §2.2 is therefore not a measure of what is unreachable, it is a measure of **how much surface the metadata work opens that a descriptor-only walker could never have named**. @@ -351,22 +351,22 @@ that costs **33 of 130 names**, and the casualty list is the API's centre of gra scale scrollable selectable shadow size toggleable transformable triStateToggleable width Overload dispatch is a larger problem than name collision (§2.5: five cases) and is unsolved here. -Python's own answer — one function that inspects its arguments — is available because the Python +Python's own answer, one function that inspects its arguments, is available because the Python side is a dispatcher, not a one-to-one mirror; see §4.4. -**Solved by `f455ef41` — by distinguishing, not by choosing.** The principle that made the drop-rule +**Solved by `f455ef41`, by distinguishing, not by choosing.** The principle that made the drop-rule right survives intact and is worth restating, because it is what rules out the obvious fix: nothing may arbitrate. `org.junit.Assert.assertEquals` has eight bindable overloads and a sort order picks the deprecated `(double, double)` that always fails, so `assertEquals(3, 3)` from Python would silently call the one that cannot work. That is still refused. What the drop-rule did not have was a way to *tell the overloads apart*: with only JVM descriptors -there is no Kotlin parameter type to name one by, and the JVM name cannot do it either — §2.3 counts +there is no Kotlin parameter type to name one by, and the JVM name cannot do it either, §2.3 counts 108 mangled JVM names that are ambiguous inside their own class. `@Metadata` supplies the declared Kotlin types, so `ArtifactScanner.disambiguateOverloads` now gives each member of a set a name of its own: -- a group of **one** keeps the bare Kotlin name — always, so a bare name still means exactly one +- a group of **one** keeps the bare Kotlin name, always, so a bare name still means exactly one declaration; - a group of more than one gets `name__`, under the **first of three schemes that separates the group**: the simple names of the declared value parameters; then the receiver joined to them @@ -375,7 +375,7 @@ own: - a group **no scheme separates** is still dropped. The old rule survives as the floor. `padding` comes out as `padding__Dp`, `padding__Dp_Dp`, `padding__Dp_Dp_Dp_Dp` and -`padding__PaddingValues` — pinned by +`padding__PaddingValues`, pinned by `ArtifactScannerTest.composeModifierExtensionsSurviveBothGates`. The receiver is left out of the suffix unless it is what separates the group, since spelling every `Modifier` extension `padding__Modifier_Dp` restates the one parameter the reader already knows from where the name is @@ -388,7 +388,7 @@ because an overload set is a property of a *package* and Kotlin lets one live in `ClassNode`s, one Kotlin name, invisible to a per-class grouping, which would have emitted both under the same table key. -The dispatcher §4.4 asks for is still wanted and still cannot live in the table — `UpcallTable` is +The dispatcher §4.4 asks for is still wanted and still cannot live in the table, `UpcallTable` is keyed by name and `ExposedCallable` carries one fixed arity, so one name reaches one signature by construction. ~~It belongs in `pythonx` (`docs/archive/pythonx-adapter-design.md` §4.1) and selects among these names.~~ **Current:** it lives in this repository's binding layer, @@ -398,7 +398,7 @@ selects among these names; the table's job is still to make that choice *possibl ### 3.2 Re-measured at `958c0082`: 314 The successor to the zero above, out of the same Gradle cache and the same -`org.jetbrains.compose.*` 1.6.11 release — though over a subset of the jars, which is why the counts +`org.jetbrains.compose.*` 1.6.11 release, though over a subset of the jars, which is why the counts below are not the ones in §2 with different values: compose: 314 declarations bound, 104 of them Modifier extensions @@ -414,15 +414,15 @@ here: 18 tests, 0 failures. | | §2 (corpus A) | §3.2 | |---|---|---| -| jars walked | 19, every `org.jetbrains.compose.*` the cache held | **6** — `foundation-layout`, `ui-unit`, `foundation`, `ui`, `material`, `material3`, all 1.6.11 desktop | -| jars on the resolution classpath | — | 15 (the six plus `ui-geometry`, `ui-graphics`, `ui-text`, `ui-util`, `runtime`, `runtime-saveable`, `animation-core`, `collection`, `annotation`, and `kotlin-stdlib`) | +| jars walked | 19, every `org.jetbrains.compose.*` the cache held | **6**, `foundation-layout`, `ui-unit`, `foundation`, `ui`, `material`, `material3`, all 1.6.11 desktop | +| jars on the resolution classpath | - | 15 (the six plus `ui-geometry`, `ui-graphics`, `ui-text`, `ui-util`, `runtime`, `runtime-saveable`, `animation-core`, `collection`, `annotation`, and `kotlin-stdlib`) | | package filter | none | `androidx.compose` | | unit counted | **declarations**, and for `Modifier` the 177 public top-level extensions over 130 distinct Kotlin names | **bindings**, after overload disambiguation | So the 104 and the 177 are not the same quantity twice. `padding` alone contributes **four** entries to the 104 and **four** declarations to the 177 but only **one** name to the 130; and the six jars -are a subset of the nineteen. The declined count is in a third unit again — 45 distinct Kotlin -*names*, deduplicated — so 104 + 45 sums to nothing meaningful. **The number of distinct Kotlin +are a subset of the nineteen. The declined count is in a third unit again, 45 distinct Kotlin +*names*, deduplicated, so 104 + 45 sums to nothing meaningful. **The number of distinct Kotlin names among the 104 bindings was not measured**, and no comparison against 130 is claimed here. The classpath is separate from the walked set for a reason §1's tooling did not have to face: @@ -431,8 +431,8 @@ so a value class or an extension receiver has to be resolvable from jars that ar being bound. **What the remaining 45 are, counted rather than asserted.** 43 of them declare a function-typed -parameter. A Python callable cannot become a Kotlin `FunctionN` at this boundary — -`UpcallTrampoline.toKotlinObject` would hand the cast a `PyObject` — so binding them would produce +parameter. A Python callable cannot become a Kotlin `FunctionN` at this boundary, +`UpcallTrampoline.toKotlinObject` would hand the cast a `PyObject`, so binding them would produce entries that always fail, and declining them is correct rather than pending. §2.2's "66 of the 177 take at least one lambda" is the same fact seen from the other side. This is the honest remaining limit on the `Modifier` chain and it is not a metadata problem. @@ -444,7 +444,7 @@ limit on the `Modifier` chain and it is not a metadata problem. The chain itself runs. `WalkedArtifactComposeModifierTest` assembles `Modifier.padding(16.dp).size(24.dp)` **from Python**, out of Compose's own jars, under Compose's own -names — no `Composer` anywhere, because §2.6 already established that a `Modifier` extension is not a +names, no `Composer` anywhere, because §2.6 already established that a `Modifier` extension is not a composable. What Python holds is an integer handle per link, and **nothing releases them**: a bare handle returned from a `CallableKind.FUNCTION` reaches Python as an `int`, which has nothing to hang a finaliser off. Three handles leak per run of that test, deliberately, because owning them is what @@ -453,13 +453,13 @@ receiver proxies, `_proxy_type` in `PythonxAdapter.kt`, SPEC U-7.)* --- -## 4. Judged — the Python surface +## 4. Judged, the Python surface ### 4.1 An extension function becomes a method on its receiver's proxy Adopted, as proposed. Chaining then costs nothing: each of the 177 `Modifier` extensions returns `Modifier`, so `Modifier.padding(16).background(...)` is ordinary Python method chaining with no -combinator machinery. This is what the 2024 attempt never reached — +combinator machinery. This is what the 2024 attempt never reached, `pythonx/compose/ui/modifier.py` is a copy of the Button wrapper in which `padding()` composes nothing and `fill_max_size()` returns `self`. @@ -475,14 +475,14 @@ Not every receiver can take a method, and the split was counted: Python cannot attach a method to `int`, so `Int.toDp` and its 19 siblings stay module-level functions on their Kotlin-named module, taking the receiver as the first positional argument. That is a real loss of fidelity and it is confined to 20 functions. (This file first wrote the result as -`pythonx.compose.ui.unit.to_dp(x)`; the binder renames nothing, and a snake_case spelling under -`pythonx` is pythonx-compose's to add.) +`pythonx.compose.ui.unit.to_dp(x)`; the binder renames no namespace -- it serves `to_dp` on +`androidx.compose.ui.unit` itself (SPEC U-12) -- and anything under `pythonx` is pythonx-compose's.) ### 4.2 `Modifier` as one name for both the type and the empty modifier `Modifier` in Kotlin is an interface, and `Modifier` as an *expression* is `Modifier.Companion`. The relevant observation is that **`Modifier$Companion` implements `androidx.compose.ui.Modifier`** -— it is an instance of the very type it is the companion of, and it is the empty modifier that +it is an instance of the very type it is the companion of, and it is the empty modifier that starts every chain. So Python does not need to reconcile two things. It needs one class whose *class object* also @@ -508,7 +508,7 @@ spellings work, and the generated `.pyi` can state all three (§4.5). > `type.__getattribute__`'s documented order: a metaclass attribute wins only if it is a **data** > descriptor, and a plain `def` on a metaclass is a non-data descriptor, so the class's own MRO is > searched first. `Modifier.padding(16)` binds `16` to `self` and raises `TypeError`. It raises only -> because the two arities differ by one — for a pair that happened to match it would bind *silently*, +> because the two arities differ by one, for a pair that happened to match it would bind *silently*, > which is the worse failure. > > **In mypy**, the same stub gives ten errors in twenty lines; every class-object spelling fails and @@ -516,15 +516,15 @@ spellings work, and the generated `.pyi` can state all three (§4.5). > > Two fixes work at run time and neither fixes the checker: a `@property` on the metaclass *is* a > data descriptor and does win, as does a hybrid descriptor in the class body with no metaclass at -> all — but **mypy does not model metaclass data-descriptor precedence** and still resolves the +> all, but **mypy does not model metaclass data-descriptor precedence** and still resolves the > class's own method. That combination is the worst available: a correct runtime with a wrong IDE. > > What resolves it is to stop making the stub mirror the runtime mechanism. Pick a runtime that > works (either of the two above) and declare each operation in the stub as a **class attribute whose -> type is a Protocol with an overloaded `__call__`** — `padding: ClassVar[_Modifier_padding]` — which +> type is a Protocol with an overloaded `__call__`**, `padding: ClassVar[_Modifier_padding]`, which > checks clean for all three spellings. `docs/archive/pyi-generation-pythonic-stubs.md` §4.4 adopts that and measured > it against mypy; PyCharm is listed there as unverified. The *observation* this section is built on -> is untouched — `Modifier$Companion` +> is untouched, `Modifier$Companion` > implements `androidx.compose.ui.Modifier`, so one name really can serve as both the type and the > empty modifier, and the three spellings really are the requirement. Only the mechanism proposed > for reaching them is retired. *(2026-10-03: the binding layer reaches both run-time spellings with a @@ -532,8 +532,8 @@ spellings work, and the generated `.pyi` can state all three (§4.5). > modifier is a Pythonic layer and belongs to pythonx-compose.)* **Rejected: binding the module-level name `Modifier` to the companion instance.** It is simpler at -runtime and reads identically in the common case, but it destroys `Modifier` as an annotation — -a type checker cannot use an instance in `m: Modifier` — and the stubs exist precisely so that a +runtime and reads identically in the common case, but it destroys `Modifier` as an annotation, +a type checker cannot use an instance in `m: Modifier`, and the stubs exist precisely so that a type checker can. ### 4.3 Collisions demand qualification; they do not get silently resolved @@ -541,8 +541,8 @@ type checker can. Confirmed, and now with a price attached: **five names** in the whole of Compose (§2.5). The default binding is the unqualified name; when two packages contribute the same `(receiver, name)`, neither wins and both are reachable only through a qualified accessor. The -same reasoning as dropping ambiguous overloads — a silent choice is a wrong call that never -reports itself — but the cost here is now known to be negligible rather than assumed to be. +same reasoning as dropping ambiguous overloads, a silent choice is a wrong call that never +reports itself, but the cost here is now known to be negligible rather than assumed to be. What is lost relative to Kotlin is real and should be said plainly: Kotlin's import scope means an extension is invisible unless imported, so an application that imports only `material3` never sees @@ -556,8 +556,8 @@ Measurement says a blanket rule in either direction is wrong. **The evidence against blanket raw coercion.** For `TextUnit`, raw `16` decodes as `Unspecified` (§2.4). For `Color`, raw `0xFFFF0000` is not red. For `Offset`, `Size`, `DpSize`, `IntSize`, -`IntOffset`, `DpOffset`, `TransformOrigin`, `CornerRadius`, `Constraints` — every `packedValue` -class, 20 of the 25 `Long`-backed ones — a user-meaningful number is not the stored number. None of +`IntOffset`, `DpOffset`, `TransformOrigin`, `CornerRadius`, `Constraints`, every `packedValue` +class, 20 of the 25 `Long`-backed ones, a user-meaningful number is not the stored number. None of these failures raises; they render wrong. **The evidence against blanket `dp(16)`.** `Dp` is 62% of value-class parameter occurrences on @@ -574,8 +574,8 @@ stores from one that packs. Whether a wrapper packs is a semantic fact with no b Adopted: -1. **Every value-class parameter accepts its proxy** — `Modifier.padding(dp(16))` always works. -2. **Raw primitives are accepted only for an explicit allowlist**, seeded with `Dp` and extended by +1. **Every value-class parameter accepts its proxy**: `Modifier.padding(dp(16))` always works. +2. **Raw primitives are accepted only for an explicit allowlist**: seeded with `Dp` and extended by hand with evidence. `Dp` covers most of the ergonomic benefit on its own. *(Current: the binder's allowlist starts empty and the seeding is pythonx-compose's, through `allow_raw_primitive`.)* 3. **Everything not on the allowlist rejects a raw number with an error that names the constructor @@ -585,7 +585,7 @@ Adopted: renders with no font size. *(Current: the binding layer refuses with "a raw number is not accepted for TextUnit: it is a value class and not on the raw-primitive allowlist …"; naming the constructor to call is left to the Pythonic layer, which owns `sp`.)* -4. **`Color` is on the reject list despite passing the constructor test**, and the allowlist +4. **`Color` is on the reject list despite passing the constructor test**: and the allowlist mechanism is manual precisely so that this exception has somewhere to live. Residual risk, stated rather than mitigated: the allowlist is a human judgement per type, and a new @@ -601,8 +601,9 @@ Kotlin module path, Kotlin parameter names, `= ...` for a Kotlin default, the ex positional-only first parameter, the declared Kotlin types as stub classes (`Modifier`, `Dp | float`, `Callable[...]`), each extension also as a callable attribute of its receiver's class, and `@overload` sets over a base name in table-key order, with the Kotlin signature in the docstring (issue #31). What -stays out is snake_case, `pythonx` and the Pythonic allowlist narrowing, which belong to pythonx-compose. The section -this replaces — the metaclass stub and the three measured requirements for a Pythonic stub — is in +stays out is `pythonx` and the Pythonic allowlist narrowing, which belong to pythonx-compose; the +snake_case aliases the runtime serves are in (SPEC U-12, B-7). The section +this replaces, the metaclass stub and the three measured requirements for a Pythonic stub, is in [`docs/archive/kotlin-extensions-in-python-pyi.md`](../archive/kotlin-extensions-in-python-pyi.md). ### 4.6 `@Composable` needs no new machinery here @@ -619,12 +620,12 @@ arity is the correct source, and the generated Kotlin passes the composer explic The "2–5" comes from §2.6's distribution, which §2.3's correction leaves unconfirmed; the nine functions re-counted in §2.3 are all `+3` or `+4`. The rule this paragraph states is what the -correction *proves* rather than what it weakens — the original nine descriptor counts were done by +correction *proves* rather than what it weakens, the original nine descriptor counts were done by hand and were all one low, for the same reason a machine would be: a `$default` mask is indistinguishable from an ordinary trailing `int` unless you already know the Kotlin arity. Two things this paragraph did not yet know. **`$default` is the third synthetic**, and Compose -carries it as a *declared* trailing parameter rather than emitting a `Button$default` bridge — so +carries it as a *declared* trailing parameter rather than emitting a `Button$default` bridge, so "call the synthetic to omit an argument" is not available for a composable at all (`docs/archive/pythonx-adapter-design.md` §4.5). And **the generated Kotlin cannot pass the composer explicitly**: `docs/archive/pythonx-adapter-design.md` §5.3 finds that Kotlin source can neither call a @@ -632,7 +633,7 @@ explicitly**: `docs/archive/pythonx-adapter-design.md` §5.3 finds that Kotlin s not parameters of the Kotlin declaration. That is a property of this generator, not of Compose, and it is why composables remain blocked while the `Modifier` chain of §3.2 runs. -**Update — composables are callable (SPEC B-6), and not through generated Kotlin.** What settled it +**Update, composables are callable (SPEC B-6), and not through generated Kotlin.** What settled it (first recorded in `docs/archive/pythonx-adapter-design.md` §5.6): - `@Composable` is `@Retention(BINARY)`: it appears in `RuntimeInvisibleAnnotations`, so the walker @@ -643,13 +644,13 @@ it is why composables remain blocked while the `Modifier` chain of §3.2 runs. - In the `$default` mask, bit *i* is parameter *i*; `$changed` is passed as `0`, the conservative value. Shifting the mask by one bit made the render fail, so the encoding is load-bearing. - A Kotlin file facade has no Kotlin name (`unresolved reference 'TextKt'`), so the call site is - emitted as **bytecode** — one `INVOKESTATIC` per composable in `ComposableThunks.kt`, name and + emitted as **bytecode**, one `INVOKESTATIC` per composable in `ComposableThunks.kt`, name and descriptor copied from the walked `MethodNode` at build time; no runtime lookup. - The binding layer (`python_multiplatform.binding`) computes the `$default` mask from which arguments the Python call wrote and threads the composer from a stack that one hand-written - `@Composable` entry pushes (`PythonComposition` in `ksp-fixtures/compose`). + `@Composable` entry pushes (`PythonComposition` in `python-multiplatform-compose/fixtures/compose`). -Proven by `ksp-fixtures/compose`'s render tests (`ComposableRenderTest`, `M3ProofRenderTest`, +Proven by `python-multiplatform-compose/fixtures/compose`'s render tests (`ComposableRenderTest`, `M3ProofRenderTest`, `CallbackDrivenRenderTest`) on desktop only; Android, iOS and wasm are planned (SPEC N-2). Extension composables and recomposition skipping remain open. @@ -663,12 +664,12 @@ suppress mangling, 4 wrappers taking `color: Long` so that `Color(...)` could be Kotlin side, and 3 taking raw `Float` sizes. That shim also invented API. `Card(modifier=…, corner_radius=20, color=…)` in `UI.ipynb` is not -Compose — `material3.Card` has no `corner_radius` parameter in either of its overloads (§2.3). The +Compose, `material3.Card` has no `corner_radius` parameter in either of its overloads (§2.3). The shim absorbed shape and colour into flat keyword arguments per component, by hand, which is exactly the per-function wrapping `docs/design/ecosystem.md` §5b rules out. The modifier chain was never built. `pythonx/compose/ui/modifier.py` lines 20–22 probe -`ButtonKt.__dict__` for a key starting `"Button-"` — a name-based lookup, which §2.3 shows cannot +`ButtonKt.__dict__` for a key starting `"Button-"`, a name-based lookup, which §2.3 shows cannot be made unambiguous for Compose no matter how carefully it is written. --- @@ -685,8 +686,8 @@ happened. dispatch receiver that Kotlin supplies implicitly from the enclosing lambda. Python has no implicit receiver, so `Modifier.weight(1f)` cannot work as written; the scope has to be passed into the content callable and the call spelled against it. No shape has been chosen. The walker - still declines them explicitly — `ArtifactScanner`'s `KotlinClassMetadata.Class` arm filters - extensions out, so nothing arrives half-bound — and `docs/design/pyi-generation-design.md` §4.5 adds the + still declines them explicitly, `ArtifactScanner`'s `KotlinClassMetadata.Class` arm filters + extensions out, so nothing arrives half-bound, and `docs/design/pyi-generation-design.md` §4.5 adds the consequence for stubs: they **must not** be stubbed as plain methods on `Modifier`, because a stub that says `Modifier.weight(1.0)` checks is a stub promising a call the runtime cannot make. - **Overload dispatch.** *Moved, not closed.* The "drop ambiguous names" rule that cost 33 of 130 @@ -695,25 +696,25 @@ happened. count and type, selecting among `padding__Dp` / `padding__Dp_Dp` / `padding__PaddingValues`. *Closed since:* it lives in this repository's binding layer (`_Overloads` in `PythonxAdapter.kt`, serving the base name; SPEC U-8), not in `pythonx`, and it interacts with §4.4 exactly as - described — if the raw `16` is what selects the `Dp` overload, then raw-primitive acceptance is + described, if the raw `16` is what selects the `Dp` overload, then raw-primitive acceptance is also overload resolution (a bare handle is not allowed to vote in a dispatch). - **The type gate.** *Closed*, by `f455ef41`. `Shape`, `Brush` and `PaddingValues` bind now, and the paragraph above misnamed what was missing: the boundary *type* (`TypeTag.OBJECT`) and its whole runtime (`HandleTable`, `marshalResult`, `toKotlinObject`) already existed; the walker lacked a Kotlin type name to cast to, which `@Metadata`'s classifier supplies. See §3's note on the type gate. What is left of the 45 declined `Modifier` extensions is **43 with a function-typed - parameter** (§3.2) — that is the lambda half of "66 take a lambda". *(Since closed in turn: + parameter** (§3.2), that is the lambda half of "66 take a lambda". *(Since closed in turn: function-typed slots bind, SPEC B-5; see §3.2's note.)* - **Handle ownership.** *New, opened by closing the type gate.* Every object crossing to Python is a strong root in `HandleTable`, and an entry bound by this walker returns it as a bare `int` with nothing to hang a finaliser off. A chained `Modifier.padding(...).size(...)` leaks one handle per intermediate link until §4.1's proxy exists to own them. Known, bounded, and true of every - `OBJECT`-returning KSP entry already — but it is now on the Compose path. *(The proxy exists now in + `OBJECT`-returning KSP entry already, but it is now on the Compose path. *(The proxy exists now in the binding layer, and SPEC M-4 asserts that owned results are released when Python drops them; the raw module still hands out bare handles.)* -- **Whether the allowlist of §4.4 should be data or code**, and where a downstream consumer adds to +- **Whether the allowlist of §4.4 should be data or code**: and where a downstream consumer adds to it for a library this repository has never seen. *Closed:* it is run-time data in the binding - layer — `_VALUE_CLASS_ALLOWLIST`, empty in this repository, extended by the Pythonic package + layer, `_VALUE_CLASS_ALLOWLIST`, empty in this repository, extended by the Pythonic package through `allow_raw_primitive` (`PythonxAdapter.kt`). The manifest candidate (`docs/archive/pyi-generation-pythonic-stubs.md` §5.3) was implemented and removed with the Pythonic stubs; it belongs to pythonx-compose. @@ -721,4 +722,4 @@ happened. `docs/design/ecosystem.md` §5b records that `LibraryAbiReader` exposes extension receivers and unerased types from klibs, which suggests the same design carries to iOS and androidNative, but no Compose klib has been read yet. *(Since: `KlibScanner` produces declarations from klibs through - `LibraryAbiReader`, but none is bound at run time yet — SPEC B-2/N-3.)* + `LibraryAbiReader`, but none is bound at run time yet, SPEC B-2/N-3.)* diff --git a/docs/design/marshalling-design.md b/docs/design/marshalling-design.md index 4f48d587d..e58b93bc7 100644 --- a/docs/design/marshalling-design.md +++ b/docs/design/marshalling-design.md @@ -31,7 +31,7 @@ inside the JVM and never crosses. | direct buffer + ASCII fast path | 373.18 | 248.94 | | **interned C string** | **160.25** | **167.33** | -Interning wins on both levels and wins *uniformly* — 160 against 167 — where composition +Interning wins on both levels and wins *uniformly*, 160 against 167, where composition differs by 2x between the same two devices. It is 3.3x better than composing on API 36 and 1.5x better on API 26. @@ -43,7 +43,7 @@ on every call is pure waste. CPython interns its own strings for the same reason The first row of the table above is labelled "was current" because it no longer describes any platform. `internedUtf8`, `encodeScratchUtf8` and `freeUtf8` are `expect`s in `jvmMain/.../ShapeDowncalls.kt` with an `actual` on desktop and Android, and the wasmJs target has -its own equivalent in `Wasm.kt` (same four-slot scratch). Android's call path uses them — +its own equivalent in `Wasm.kt` (same four-slot scratch). Android's call path uses them, `EmbedAPI.android.kt`'s `PyObject_GetAttrString` goes through `internedUtf8`, so the live cost is the bottom row, not the top one. Quoting 2238 ns as Android's `getAttr` cost is wrong, and it has been quoted that way. @@ -58,8 +58,8 @@ Do not mix the two primitives up. They exist for different lifetimes: | | for | lifetime | |---|---|---| -| `internedUtf8` | repeated identifiers — attribute, module, method names | kept, bounded at 4096 entries | -| `encodeScratchUtf8` | arbitrary content — `exec` source, user strings | thread-local scratch, freed on the next call | +| `internedUtf8` | repeated identifiers, attribute, module, method names | kept, bounded at 4096 entries | +| `encodeScratchUtf8` | arbitrary content, `exec` source, user strings | thread-local scratch, freed on the next call | Interning an `exec` source string would blow the cache; intern only where the argument is a name. @@ -67,7 +67,7 @@ Interning an `exec` source string would blow the cache; intern only where the ar Centre the abstraction on **interning**, not on a general allocator. - // jvmMain — shared by Android and desktop (jvmMain/.../ShapeDowncalls.kt) + // jvmMain, shared by Android and desktop (jvmMain/.../ShapeDowncalls.kt) @PublishedApi internal expect fun internedUtf8(s: String): Long internal expect fun encodeScratchUtf8(s: String): Long internal expect fun freeUtf8(address: Long) @@ -80,7 +80,7 @@ free. **The scratch pool holds four slots per thread**, handed out round-robin, on both desktop and Android. One slot would have been enough for the "encode, then immediately pass to C" pattern, -but not for a call that needs two arbitrary strings at once — +but not for a call that needs two arbitrary strings at once, `PyImport_ExecCodeModuleWithPathnames` needs two, `PyUnicode_DecodeLocale` needs two. Four leaves headroom without pretending the addresses last. The fifth consecutive encode on a thread invalidates the first, and `StringMarshallingTest.testMultiSlotScratchReuseLimit` pins exactly @@ -94,13 +94,13 @@ that boundary, so it is a tested guarantee rather than an implementation detail. `filename` and scratches `source`; they are arguments to the same call. Desktop and Android make the same split for the same argument, and `desktopMain/.../bindings.kt` is the reference. -| interned — the argument is a name | scratched — the argument is content | +| interned, the argument is a name | scratched, the argument is content | |---|---| | attribute names (`PyObject_*AttrString*`) | source text (`Py_CompileString.str`, `PyRun_*`) | | dict / mapping keys (`PyDict_*ItemString`, `PyMapping_*String`) | exception and warning messages | | module names (`PyImport_*`, `PySys_*`, `PyErr_NewException.name`) | file paths a module was loaded *from* (`pathname`, `cpathname`) | | codec names (`encoding`) | error-handler names (`errors`), comparands, docstrings, `PySequence_Fast.m` | -| `PyUnicode_InternFromString.str` — the caller is asserting it repeats | `PyBytes_FromString.v` — payload | +| `PyUnicode_InternFromString.str`, the caller is asserting it repeats | `PyBytes_FromString.v`, payload | Two cases are worth stating because they read the wrong way at a glance: @@ -115,7 +115,7 @@ the four-slot boundary. ### Cache policy -Interning without a bound is a leak, because callers can pass arbitrary strings — an attribute +Interning without a bound is a leak, because callers can pass arbitrary strings, an attribute name built at runtime, a code fragment. The cache therefore: - holds a bounded number of entries; beyond that, misses take the scratch path instead of @@ -124,7 +124,7 @@ name built at runtime, a code fragment. The cache therefore: - never frees an interned entry, which is what makes the returned address safe to hold The bound turns "unbounded growth" into "predictable ceiling, then fall back to scratch". A -literal-heavy workload — which is the normal case — stays entirely in the cache. +literal-heavy workload, which is the normal case, stays entirely in the cache. ### ASCII fast path @@ -142,7 +142,7 @@ full UTF-8 only when it sees a character above 0x7F. Android's one JNI call per buffer is amortised to nothing; what matters is that no crossing happens per *string*. That is the same shape Panama has on desktop, and the same shape -PanamaPort would have provided — its `allocateFrom(String)` runs through +PanamaPort would have provided, its `allocateFrom(String)` runs through `AndroidUnsafe.allocateMemory` to `sun.misc.Unsafe`, a JVM intrinsic with no crossing. ### What this replaces @@ -152,7 +152,7 @@ Composition for string-carrying operations, at every API level. `getAttr` compos native function, and no JVM-side wiring. It also applies to every string-carrying operation at once rather than one at a time. -Composition remains justified only where crossing count genuinely scales with N — bulk +Composition remains justified only where crossing count genuinely scales with N, bulk iteration, measured at 11x for a 1000-element list. ## Non-goals diff --git a/docs/design/object-lifetime.md b/docs/design/object-lifetime.md index a33b44e14..b843d81ed 100644 --- a/docs/design/object-lifetime.md +++ b/docs/design/object-lifetime.md @@ -3,7 +3,7 @@ Two runtimes, two memory models, and no shared view between them. This is what has been settled, what is measured, and what is still open. -## Kotlin holding Python — solved and verified +## Kotlin holding Python, solved and verified CPython counts references; a Kotlin wrapper takes one on construction and gives it back when it closes. @@ -14,7 +14,7 @@ closes. the Kotlin wrapper closes refcount 0, freed `RefCountTest` checks this in both directions, because both failure modes are silent: releasing -one too few leaks, and releasing one too many frees an object still in use — that crash lands +one too few leaks, and releasing one too many frees an object still in use, that crash lands somewhere unrelated, which is how the interpreter corruption during the lifetime work first appeared, as a segfault inside `Py_Finalize` far from its cause. @@ -31,7 +31,7 @@ library therefore takes a periodic checkpoint itself (`Python3.drainPendingRelea `autoDrainInterval`, on by default only for free-threaded builds). See ROADMAP §9, `docs/investigations/gc-scheduling-investigation.md` and [`threading-and-abi.md`](threading-and-abi.md). -## Python holding Kotlin — the handle table +## Python holding Kotlin, the handle table There is no counterpart to `Py_INCREF` for a JVM or Kotlin/Native object, so something on the Kotlin side has to act as a GC root: @@ -48,7 +48,7 @@ Python holds only the integer handle; the map holds the object. This is what JNI The sketch above has no answer to that, and neither did the implementation. `HandleTable` is generational precisely so that a double release is safe, and its class doc named the counterpart -exactly — *"the proxy's `tp_dealloc` calling `release` is the entire lifetime contract"* — but +exactly, *"the proxy's `tp_dealloc` calling `release` is the entire lifetime contract"*, but `PythonProxySource` rendered a proxy class whose `__init__` took a handle and which had no path back at all. Every Kotlin object Python ever constructed stayed rooted for the life of the process. @@ -64,13 +64,13 @@ Two separate defects wore that one number, and it is worth keeping them apart: | a proxy instance (`Counter(10)`) | the Python object | the **library**: no `__del__` was rendered. Fixed; `ProxyHandleLifetimeTest` pins it, including for a proxy inside a reference cycle | | a bare handle from `_pm_invoke` on a CONSTRUCTOR entry | the **caller** | nothing was wrong with the library. An integer has nothing to hang a finaliser off, so the caller must call `_pm_release`; the measurement harness was not doing it | -The leak was **identical on desktop and on the iOS simulator** — 64 handles registered, 64 still -live after `gc.collect()`, on both — which is what one would expect of a defect in generated Python +The leak was **identical on desktop and on the iOS simulator**, 64 handles registered, 64 still +live after `gc.collect()`, on both, which is what one would expect of a defect in generated Python that `commonMain` renders. The earlier impression that "iOS was stable" was about the *measurement* and not about the table: the iOS boundary costs about 3.5 µs against desktop's 0.5 µs, so the same drift was proportionally invisible there. -`__del__` was chosen over `weakref.finalize` on price — the alternatives and their measured costs +`__del__` was chosen over `weakref.finalize` on price, the alternatives and their measured costs are tabulated in `PythonProxySource`'s KDoc. The pre-PEP-442 objection to `__del__` (an object with one, inside a cycle, was never finalised at all) has not applied since CPython 3.4, and the cycle case is asserted rather than assumed. @@ -80,7 +80,7 @@ case below is untouched. What changed is only that "Python has finished with it" at all, which is the precondition for anything else. A note for anyone reading older discussion: Panama has no equivalent of `NewGlobalRef`, and it -does not need one. Panama is an API for *native memory*, not for JVM object lifetime — a +does not need one. Panama is an API for *native memory*, not for JVM object lifetime, a `MemorySegment` holding an address does not keep a Kotlin object alive. The GC root is an ordinary Kotlin data structure, not anything Panama provides. Looking for the answer inside Panama was looking at the wrong layer. @@ -89,7 +89,7 @@ Panama was looking at the wrong layer. ### Why reference counting cannot do it -Not a boundary problem — an inherent limit of reference counting, in any language: +Not a boundary problem, an inherent limit of reference counting, in any language: a = []; b = [] a.append(b); b.append(a) @@ -104,7 +104,7 @@ CPython's algorithm is simple, and it turns on one requirement: 1. each tracked object's `tp_traverse` enumerates the `PyObject`s it holds 2. references discovered that way are subtracted from the refcount -3. if the remainder is zero, the object is kept alive only from inside the cycle — collectable +3. if the remainder is zero, the object is kept alive only from inside the cycle, collectable 4. `tp_clear` drops those references and breaks the cycle **Everything depends on step 1 being able to see through whatever holds the reference.** @@ -138,7 +138,7 @@ fields are `PyObject`-typed. A traverse entry is one more generated function: obj.otherField?.let { visit(it.pointer.toRawValue()) } } -No reflection — which is the whole reason the table exists. A reflection-based binding would +No reflection, which is the whole reason the table exists. A reflection-based binding would find this expensive; that is why mature ones (JPype and similar) document "do not create cycles" instead of solving it. We are not in that position. @@ -166,7 +166,7 @@ strongly, so it is a root regardless. **Still open.** The handle table holds strongly (`HandleTable.kt` notes it "needs to hold weakly" for this case; ROADMAP §14b records it as untouched). The likely answer is to make the map hold weakly, -with the strong reference supplied by the Python proxy for as long as it lives — so "Python stopped +with the strong reference supplied by the Python proxy for as long as it lives, so "Python stopped using it" becomes "the JVM may collect it". That inverts who owns the strength and has not been worked through. @@ -174,7 +174,7 @@ worked through. An earlier discussion concluded that "cycles are handled by each language's GC" on the strength of the four-step refcount sequence at the top of this document. That sequence is correct, and it -is implemented and tested — but it describes the **non-cyclic** case. Step 4, "the Kotlin side +is implemented and tested, but it describes the **non-cyclic** case. Step 4, "the Kotlin side releases", is exactly what a cycle prevents from ever happening. The claim does not follow, and the same discussion walked it back a few messages later. @@ -183,7 +183,7 @@ the same discussion walked it back a few messages later. `PyContext` chooses how far a Python value is converted toward Kotlin, per `ConversionStrategy`; `withContext` swaps and restores the strategy even on throw, and all five variants dispatch. `PyValue`'s lazy path is filled in too: the stub that used to end in -`cachedNativeValue!!` — an NPE on any genuine cache miss — is gone. No `!!` on that field remains +`cachedNativeValue!!`, an NPE on any genuine cache miss, is gone. No `!!` on that field remains anywhere in the library sources; the only occurrence of the expression left in the tree is a comment in `ConversionTest` recording that it used to be there. @@ -196,23 +196,23 @@ actually returns, not from what the type looks like. ### The rule, per source type `PyProxy.toKotlinOrNull` converts through `pyObjectToNative`, whose dispatch compares the source's -**exact** type object against `PyTypeChecks`' cached builtin types — `PyLong_Check` semantics, not +**exact** type object against `PyTypeChecks`' cached builtin types, `PyLong_Check` semantics, not `isinstance`. So the table is by exact type; a subclass falls through to the last row. | Python type | Conversion | Kotlin result | Points into Python memory? | Cached? | |---|---|---|---|---| | `None` | `PyNone.isNone`, a pointer comparison | `null` | no | re-derived; `null` cannot be told apart from "not computed", and the comparison costs no FFI call | -| `bool` | `PyLong_AsLongLong(...) != 0` | `Boolean` | no — a scalar copy | yes | -| `int` | `PyLong_AsLongLong` | `Long` | no — a scalar copy (64-bit range only; wider ints do not round-trip) | yes | -| `float` | `PyFloat_AsDouble` | `Double` | no — a scalar copy | yes | -| `str` | `PyUnicode_AsUTF8` | `String` | **the C call returns a `const char*` owned by CPython**, but that pointer never leaves the binding: `EmbedAPI`'s `expect` returns `String?`, and each platform decodes at the boundary (`toKString()` on Kotlin/Native, `Panama.readUtf8String` on desktop, `Wasm.readUtf8String` on wasmJs). The `String` is a copy. | yes — the `String`, never the pointer | -| `list`, `tuple` | `toNativeList()`, recursive | `List` | no — every element is itself one of the rows above | yes, as a **snapshot** | +| `bool` | `PyLong_AsLongLong(...) != 0` | `Boolean` | no, a scalar copy | yes | +| `int` | `PyLong_AsLongLong` | `Long` | no, a scalar copy (64-bit range only; wider ints do not round-trip) | yes | +| `float` | `PyFloat_AsDouble` | `Double` | no, a scalar copy | yes | +| `str` | `PyUnicode_AsUTF8` | `String` | **the C call returns a `const char*` owned by CPython**, but that pointer never leaves the binding: `EmbedAPI`'s `expect` returns `String?`, and each platform decodes at the boundary (`toKString()` on Kotlin/Native, `Panama.readUtf8String` on desktop, `Wasm.readUtf8String` on wasmJs). The `String` is a copy. | yes, the `String`, never the pointer | +| `list`, `tuple` | `toNativeList()`, recursive | `List` | no, every element is itself one of the rows above | yes, as a **snapshot** | | `dict` | `toNativeMap()`, recursive over a snapshot of the entries | `Map` | no | yes, as a **snapshot** | | `set`, `frozenset` | `toNativeSet()`, recursive over `PySequence_Tuple` | `Set` | no | yes, as a **snapshot** | -| anything else *inside a container* | `pyObjectToNative`'s `else`: `str(obj)` | `String` | no — a copy, but lossy and one-way | yes | -| `bytes`, `bytearray` | `bytes.hex()` across the boundary, decoded to bytes on this side | `ByteArray` | yes — a copy, and `bytearray` a **snapshot** | yes | -| `memoryview` | **none — refused** | `null` / `PyTypeError` | the buffer protocol hands out a pointer *into* the object, and a copy would still not carry format, shape and strides | never | -| user-defined classes, subclasses of builtins, `complex`, ... | **none — refused** | `null` / `PyTypeError` | — | never | +| anything else *inside a container* | `pyObjectToNative`'s `else`: `str(obj)` | `String` | no, a copy, but lossy and one-way | yes | +| `bytes`, `bytearray` | `bytes.hex()` across the boundary, decoded to bytes on this side | `ByteArray` | yes, a copy, and `bytearray` a **snapshot** | yes | +| `memoryview` | **none, refused** | `null` / `PyTypeError` | the buffer protocol hands out a pointer *into* the object, and a copy would still not carry format, shape and strides | never | +| user-defined classes, subclasses of builtins, `complex`, ... | **none, refused** | `null` / `PyTypeError` | - | never | The container rows are the answer to "what happens when the elements are themselves `PyObject`s": they are not. `pyObjectToNative` recurses, so a converted container bottoms out in `Long`, @@ -228,7 +228,7 @@ staleness and the recovery are both pinned by tests. | `ConversionStrategy` | Hands back | Cacheable | |---|---|---| -| `RAW` | a bare `NativePointer` — no wrapper, no incref, nothing that releases it | **never.** It is an address, not a reference. `PyValue`'s constructor rejects one as `initialNativeValue`, because storing it would fail only once the address had been reused, far from the cause | +| `RAW` | a bare `NativePointer`, no wrapper, no incref, nothing that releases it | **never.** It is an address, not a reference. `PyValue`'s constructor rejects one as `initialNativeValue`, because storing it would fail only once the address had been reused, far from the cause | | `UNMANAGED` | the `PyObject` unchanged, under Python's own refcounting | no native value exists to cache | | `TYPED` / `DEFAULT` | a `PyValue` over a wrapper it owns a reference to | per the type table above | | `NATIVE` | the fully-converted value, eagerly | nothing holds it; there is no cache in this path | @@ -239,7 +239,7 @@ staleness and the recovery are both pinned by tests. scalars, `String`, `ByteArray` and containers built recursively out of those; `false` for a `NativePointer` (an unowned address) and for a `PyObject` (a reference, but not a native value), and false for anything unrecognised. Every store to `cachedNativeValue` on the conversion path -passes through it, and a value it rejects is still returned to the caller — it is simply +passes through it, and a value it rejects is still returned to the caller, it is simply re-converted next time instead of kept. Today every branch the walk can reach passes; the guard exists so a branch added later cannot acquire a cache without someone deciding that it should. @@ -248,12 +248,12 @@ exists so a branch added later cannot acquire a cache without someone deciding t The Python side has the opposite rule: `cachedPyObjectValue` must hold an **owned** reference, never a borrowed one, because the proxy can outlive whatever handed the object over. -`PyContext.proxyConvert` did not honour it. `typedWrap` returns a fresh wrapper — built -`borrowed = true`, i.e. with its own incref — for the nine builtins it knows, and returns *the +`PyContext.proxyConvert` did not honour it. `typedWrap` returns a fresh wrapper, built +`borrowed = true`, i.e. with its own incref, for the nine builtins it knows, and returns *the caller's wrapper unchanged* for everything else. On that second path the `PyValue` held no reference of its own, so a caller closing its wrapper (which callers do; `hasNativeCounterpart` does it to its own scratch wrapper) left the `PyValue` pointing at a released object. Measured as -a missing `+1` on `sys.getrefcount` — 3 where 4 was required — by +a missing `+1` on `sys.getrefcount`, 3 where 4 was required, by `PyValueLazyConversionTest.pyContextGivesEveryPyValueItsOwnReference`, and fixed by taking an independent reference on the fallback path. Both branches now hand the `PyValue` a wrapper nobody else holds. @@ -269,7 +269,7 @@ else holds. What changed is that the buffer is not the only route. `bytes.hex()` is exact, pure ASCII, and costs three FFI crossings whatever the length, where walking the sequence costs one per byte. The price is four passes over the payload where a direct copy would make one, which makes this - the largest per-byte cost in the conversion layer — the case for adding a real + the largest per-byte cost in the conversion layer, the case for adding a real `PyBytes_AsStringAndSize` binding later. That binding is not free either: a non-primitive value would have to cross four platform boundaries against `androidMain`'s primitives-only rule. - **Subclasses of builtins are refused** because the dispatch is by exact type. That is diff --git a/docs/design/pyi-generation-design.md b/docs/design/pyi-generation-design.md index 59eb1732c..2e5b0c69f 100644 --- a/docs/design/pyi-generation-design.md +++ b/docs/design/pyi-generation-design.md @@ -1,9 +1,9 @@ -# Generating `.pyi` — the reference implementation, the input, and what a stub can actually say +# Generating `.pyi`, the reference implementation, the input, and what a stub can actually say > **Revised 2026-10-03.** This file now describes only the stub product this repository ships: `.pyi` > under the **Kotlin** module paths, with Kotlin names (SPEC B-7). The design of a second, Pythonic -> product — `pythonx.*` stubs, snake_case, `Dp | float`, `@overload` sets, the `Modifier` -> callback-Protocol shape and the `pythonx-map.toml` manifest — was removed from the plugin in +> product, `pythonx.*` stubs, snake_case, `Dp | float`, `@overload` sets, the `Modifier` +> callback-Protocol shape and the `pythonx-map.toml` manifest, was removed from the plugin in > `238119b7` and moved to > [`docs/archive/pyi-generation-pythonic-stubs.md`](../archive/pyi-generation-pythonic-stubs.md); > that product belongs to pythonx-compose. Section numbers are kept so that the code's KDoc @@ -14,7 +14,11 @@ > its receiver's class as well, and states function types, nullability, value classes and overload > sets (§3.1, §3.3–§3.6, §4.1). Everything is still under Kotlin names; the "`Dp | float`, `@overload` > sets, `Modifier` callback-Protocol shape" that the paragraph above says left the plugin are back -> **as Kotlin-name typing**, not as a Pythonic product -- no snake_case, no `pythonx`. +> **as Kotlin-name typing**, not as a Pythonic product -- no `pythonx`. +> +> **Revised again for issue #131.** snake_case is back -- not as a `pythonx` product, but as the +> Pythonic aliases the runtime serves on the Kotlin-named modules themselves (SPEC U-12): parameters +> under their Pythonic keyword, and `alias = kotlinName` beside each module-level name (§3.6). The binder serves Kotlin-named modules (`androidx.compose.foundation.layout`, `junit.runner.Version`, a consumer's own package) that exist only as `sys.modules` entries made at run time @@ -29,7 +33,7 @@ does not revisit it. What this file answers: **what the generator reads**, **what it writes**, and **where the result goes**. The generator is `PythonStubsTask` + `renderKotlinFqnStubs` (`PyiRendering.kt`, in `python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/`); its tests are -`PyiRenderingTest`, `KotlinNamesOnlyStubTest` and `ksp-fixtures/artifact`'s `WalkedArtifactStubTest`. +`PyiRenderingTest`, `KotlinNamesOnlyStubTest` and `python-multiplatform-gradle-plugin/fixtures/artifact`'s `WalkedArtifactStubTest`. Everything under "Observed" was read from working copies or run as a command on 2026-08-14, with the path or the command given, unless dated otherwise. Everything under "Judged" is a decision, and the @@ -45,7 +49,7 @@ alternatives it was chosen over are named. "Current" statements were checked aga | what | where | what it settled | |---|---|---| | PyREPL's generator | `/Volumes/macMini/PyREPL/app/build.gradle.kts` (627 lines, HEAD `899c6bf`, 2024-09-01, remote `github.com/thisisthepy/PyREPL`) | §1 in full | -| toolchain's mutated copy | `/Volumes/macMini/thisisthepy/toolchain/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dependency/lang/kotlin/meta/createMetaClass.kt` (248), `meta/MetaPackageBuildTask.kt` (64), `kotlin/decompileKotlinMeta.kt` (145) | §1.3 — and it corrects `docs/design/ecosystem.md` | +| toolchain's mutated copy | `/Volumes/macMini/thisisthepy/toolchain/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/dependency/lang/kotlin/meta/createMetaClass.kt` (248), `meta/MetaPackageBuildTask.kt` (64), `kotlin/decompileKotlinMeta.kt` (145) | §1.3, and it corrects `docs/design/ecosystem.md` | | the artefact walker | `python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/{ArtifactScanner,KotlinMetadata,ArtifactRendering,JvmDescriptors,PythonArtifactBindingsTask}.kt` | §2 | | the KSP producer | `python-multiplatform-ksp/src/main/kotlin/python/multiplatform/ksp/{FragmentScanner,Model,TypeShape,KsAdapters}.kt` | §2 | | the runtime proxy renderer | `python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/PythonProxySource.kt` | §2.2, §6 | @@ -57,13 +61,13 @@ alternatives it was chosen over are named. "Current" statements were checked aga mypy **2.3.0** in a throwaway venv at `/Volumes/macMini/tmp/pyicheck` (nothing was installed into the repository, and no repository file other than this one was touched). Eight checks, reported in -§4.3, §4.4 and §6.1 — §4.3, §4.4 and most of §6.1 are now in the archive file named at the top. +§4.3, §4.4 and §6.1, §4.3, §4.4 and most of §6.1 are now in the archive file named at the top. CPython 3.13 for the two runtime attribute-lookup checks in §4.3. ### Not read, and therefore not claimed -- **`github.com/thisisthepy/PyREPL`'s files.** The repository page was fetched and exists — public, - described as *"UI-Enabled Python REPL for Android/iOS/Desktop"*, 7 stars, 9 forks — but the fetch +- **`github.com/thisisthepy/PyREPL`'s files.** The repository page was fetched and exists, public, + described as *"UI-Enabled Python REPL for Android/iOS/Desktop"*, 7 stars, 9 forks, but the fetch returned no file contents. **Everything said about PyREPL here comes from the local clone at `/Volumes/macMini/PyREPL`**, whose `origin` is that URL. Whether the clone is current with the remote was not checked. @@ -77,12 +81,12 @@ CPython 3.13 for the two runtime attribute-lookup checks in §4.3. throughout, and not asserted.** - **klib.** Everything here is the JVM path. `docs/design/ecosystem.md` §5b records that `LibraryAbiReader` returns Kotlin qualified names, extension-receiver flags and unerased types - from a klib, which is the same information §2 says a stub needs — but no klib was read for this + from a klib, which is the same information §2 says a stub needs, but no klib was read for this document. --- -## 1. Observed — PyREPL's generator +## 1. Observed, PyREPL's generator ### 1.1 The pipeline @@ -98,11 +102,11 @@ set makes a **resolvable copy** of its `implementation` and `api` configurations then takes `resolvedConfiguration.lenientConfiguration.artifacts`. Unresolved dependencies that name a sibling project are recursed into with `isSubModule = true`, which switches the copy to `api` only -— the same visibility rule Gradle itself applies. Everything else is collected as genuinely +the same visibility rule Gradle itself applies. Everything else is collected as genuinely unresolved and printed. The output directory is derived per source set: from `sourceSet.kotlin.srcDirs`, take the one under -`/kotlin`, and emit into its sibling `generated/meta` — i.e. `src//generated/meta/`, +`/kotlin`, and emit into its sibling `generated/meta`, i.e. `src//generated/meta/`, **inside the source tree, not the build directory**. That directory is then registered as a Python source root through the only mechanism the project had for one: @@ -111,7 +115,7 @@ source root through the only mechanism the project had for one: "src/commonMain/python", "src/commonMain/generated/meta") } } } A `build/kotlin-python/meta_json.lock` records the resolved artefact set (group, name, version, -file) as JSON so a changed dependency graph can be detected. **It is computed and then ignored** — +file) as JSON so a changed dependency graph can be detected. **It is computed and then ignored**, `outputs.upToDateWhen { false }` and the `if (!isChanged) return@doLast` guard is commented out, so the task runs every time. @@ -120,7 +124,7 @@ for each class whose binary name has no `$` and which is not `private`/`protecte ### 1.2 What it writes -One `__init__.pyi` **per Java package**, at `createInitPythonFile` (line 368) — the file is named +One `__init__.pyi` **per Java package**, at `createInitPythonFile` (line 368), the file is named after the package directory, and every class in that package is *appended* to it. Bodies are `...`; there is no runtime in the file at all. @@ -131,7 +135,7 @@ there is no runtime in the file at all. def main(self, *args, **kwargs): ... Filters, from `generateMetaPythonClass` (line 375): drop `private`/`protected`; drop fields starting -`$` or `Companion`; drop methods starting `<`; **drop any member whose name contains `-`** — the +`$` or `Companion`; drop methods starting `<`; **drop any member whose name contains `-`**, the value-class mangling suffix, discarded rather than decoded; skip numeric (anonymous) nested classes; emit `...` for a class that ends up empty. @@ -144,18 +148,18 @@ Collisions between `commonMain` and a platform source set are handled by `checkF (line 525): find `__init__.pyi` paths present in both trees, rename **both** to `_.pyi` and `__.pyi`, and write a new `__init__.pyi` that re-exports both with `from ._X import *`. `findCommonDir` and `reCreateFile` split on the literal `"\\__init__.pyi"`, so -**this path is Windows-only** and silently does nothing on macOS or Linux — `docs/design/ecosystem.md` +**this path is Windows-only** and silently does nothing on macOS or Linux, `docs/design/ecosystem.md` already flagged the separators; what is added here is that the failure is silent rather than an error. -### 1.3 The two lineages — and a correction to `docs/design/ecosystem.md` +### 1.3 The two lineages, and a correction to `docs/design/ecosystem.md` `docs/design/ecosystem.md` §2 says `dba17dd` "extracted PyREPL's meta-generation" into `createMetaClass.kt` and that `toolchain`'s copy is "the same code mutated into a *runtime* generator". The extraction is right; the implied direction is not. -`git show f0dd368:app/build.gradle.kts` — the commit in **toolchain's own history** that first added -the generator, one day before the extraction — already emits `.py`, not `.pyi`: +`git show f0dd368:app/build.gradle.kts`, the commit in **toolchain's own history** that first added +the generator, one day before the extraction, already emits `.py`, not `.pyi`: fun createMetaPythonFile(node: ClassNode, outputDir: File): File { ... @@ -173,7 +177,7 @@ is larger than "delete the bodies". One thing in `toolchain` is worth keeping regardless of lineage: `decompileKotlinMeta.kt` already reaches for **`kotlinx-metadata-jvm`** and dispatches on `KotlinClassMetadata.Class` / `FileFacade` / `MultiFileClassPart` / `MultiFileClassFacade`. It is a standalone `main()`, wired into -no task, and its output is `def name(self): pass` with no types — but the instinct is the same one +no task, and its output is `def name(self): pass` with no types, but the instinct is the same one this repository's `KotlinMetadata.kt` acted on, two years later. ### 1.4 What survives the port @@ -193,7 +197,7 @@ this repository's `KotlinMetadata.kt` acted on, two years later. --- -## 2. The input — judged +## 2. The input, judged ### 2.1 Observed (2026-08-14): what the two producers kept then @@ -212,28 +216,28 @@ field-for-field near-identical: | `arity` | yes | yes | | `paramTags` | `List` | `List` | | `returnTag` | `Tag` | `String` | -| `kind` | `"FUNCTION"`, `"METHOD"`, … | — (always FUNCTION) | +| `kind` | `"FUNCTION"`, `"METHOD"`, … |, (always FUNCTION) | | `lambdaBody` | generated Kotlin source text | generated Kotlin source text | -| `isSuspend` | yes | — (always false) | -| `imports` | — | extension-call aliases | +| `isSuspend` | yes |, (always false) | +| `imports` | - | extension-call aliases | -`Tag` (`TypeShape.kt` line 9) is `INT | FLOAT | BOOLEAN | STRING | BYTES | UNIT | OBJECT` — seven +`Tag` (`TypeShape.kt` line 9) is `INT | FLOAT | BOOLEAN | STRING | BYTES | UNIT | OBJECT`, seven values, and `OBJECT` means "a handle, contents unknown". The runtime's `ExposedCallable`, which `PythonProxySource.render` consumes, carries the same and nothing more. **What is therefore absent from every existing representation:** -- parameter **names** — `FragmentScanner` does `function.parameters.map { it.type.toShape() }` and +- parameter **names**: `FragmentScanner` does `function.parameters.map { it.type.toShape() }` and drops `it.name`; `ArtifactScanner` does the same to `KmValueParameter` -- **defaults** — `KmValueParameter.declaresDefaultValue` is read by nobody -- **nullability** — `resolveKotlinType` returns `null` for a nullable type outright +- **defaults**: `KmValueParameter.declaresDefaultValue` is read by nobody +- **nullability**: `resolveKotlinType` returns `null` for a nullable type outright (`KotlinMetadata.kt` line 206), so the fact never leaves the scanner -- the **declared Kotlin type** — `Dp` becomes `Tag.FLOAT`, `List` becomes `Tag.OBJECT` -- whether a parameter is an **extension receiver** — known inside `buildCallableFromFunction`, spent +- the **declared Kotlin type**: `Dp` becomes `Tag.FLOAT`, `List` becomes `Tag.OBJECT` +- whether a parameter is an **extension receiver**, known inside `buildCallableFromFunction`, spent on choosing `receiver.alias(...)`, then discarded -- **overload grouping** — both producers *delete* the information: `ArtifactScanner.scanClassNode` +- **overload grouping**: both producers *delete* the information: `ArtifactScanner.scanClassNode` line 230 keeps only names with exactly one candidate; `FragmentScanner` keeps the first -- value-class identity — `ValueClassInfo` exists (`KotlinMetadata.kt` line 168) and is consumed +- value-class identity, `ValueClassInfo` exists (`KotlinMetadata.kt` line 168) and is consumed inside a closure to build a wrap/unwrap expression, never surfaced ### 2.2 Judged: the input is a third representation, upstream of both renderers @@ -241,7 +245,7 @@ values, and `OBJECT` means "a handle, contents unknown". The runtime's `ExposedC **Neither producer's output can drive a `.pyi` generator, and neither should be widened to try.** The reasoning is the one `PythonProxySource`'s KDoc already gives for a different split: an -`ExposedCallable` is *"exactly the inputs"* for the runtime proxy, and that is the point — it was cut +`ExposedCallable` is *"exactly the inputs"* for the runtime proxy, and that is the point, it was cut down to exactly what a trampoline needs. A `.pyi` needs a strict superset of what a *call site* needs, because a stub describes a signature and a call site only has to produce one. Growing `CallableEntryModel` to carry parameter names and defaults would put fields into the model that @@ -268,7 +272,7 @@ what crosses, not what Kotlin declares (§3.1); and of the annotations it keeps function, a member, a constructor, a property; the extension receiver's Kotlin type or `null`; an ordered parameter list of `(name, KotlinTypeModel, declaresDefault)`; the return `KotlinTypeModel`; `isSuspend`; and the annotations that matter (`@Composable`, `@Deprecated`). `KotlinTypeModel` -carries a qualified name, nullability, type arguments, and — when the classifier is a value class — +carries a qualified name, nullability, type arguments, and, when the classifier is a value class, its underlying `KotlinTypeModel` plus the two visibility booleans `ValueClassInfo` already computes. Three properties this buys, each of which is a defect in the current shape: @@ -281,7 +285,7 @@ Three properties this buys, each of which is a defect in the current shape: them (§3.6). Those are different decisions and today they are the same line of code. *(Since then the binder stopped dropping them: each member gets its own `name__` key, §3.6.)* 3. **What is declined stays visible.** A model entry can be marked "not bound, reason X" and still - be stubbed — or deliberately not stubbed, which is the honest choice when the runtime cannot call + be stubbed, or deliberately not stubbed, which is the honest choice when the runtime cannot call it. Today a declined declaration returns `null` and vanishes. **Rejected alternatives:** @@ -290,15 +294,15 @@ Three properties this buys, each of which is a defect in the current shape: |---|---| | generate `.pyi` from the runtime `UpcallTable` (mirroring `PythonProxySource`) | the table holds `ExposedCallable`, i.e. tags and arity. Every parameter would be `a0, a1, a2` with no type. It is also the wrong time: `PythonProxySource` renders at run time *because* the installed set is only known then, and a stub has to exist before anything runs | | widen `CallableEntryModel` / `ArtifactCallable` in place | puts fields on the hot path that its renderer must ignore, and does not fix the KSP/ASM asymmetry (`kind` and `isSuspend` on one, `imports` on the other) | -| a second scan, independent of the binder's | two readers of the same jars that can disagree — precisely the failure `PythonProxySource` §"Where this is generated" refuses ("a second generator that can disagree with the first") | +| a second scan, independent of the binder's | two readers of the same jars that can disagree, precisely the failure `PythonProxySource` §"Where this is generated" refuses ("a second generator that can disagree with the first") | | emit `.pyi` from KSP as well as from the plugin | KSP does not see third-party artefacts at all, which is the entire reason the artefact walker exists (`docs/design/ecosystem.md` §5b). The plugin sees both | ### 2.3 What each producer has to supply | producer | has it today | needs | |---|---|---| -| KSP | `KSFunctionDeclaration` gives names, defaults (`KSValueParameter.hasDefault`), nullability, type arguments, annotations | nothing new read — only *kept*. `KsAdapters.renderWithArguments` already walks type arguments | -| artefact walker | `KmFunction` gives `valueParameters[].name`, `.declaresDefaultValue`, `KmType.isNullable`, `.arguments`, `receiverParameterType`; `ValueClassInfo` gives the rest | nothing new read — only *kept*. `functionsOf` currently throws away `KmValueParameter` and keeps `.type` | +| KSP | `KSFunctionDeclaration` gives names, defaults (`KSValueParameter.hasDefault`), nullability, type arguments, annotations | nothing new read, only *kept*. `KsAdapters.renderWithArguments` already walks type arguments | +| artefact walker | `KmFunction` gives `valueParameters[].name`, `.declaresDefaultValue`, `KmType.isNullable`, `.arguments`, `receiverParameterType`; `ValueClassInfo` gives the rest | nothing new read, only *kept*. `functionsOf` currently throws away `KmValueParameter` and keeps `.type` | | Java classes in a walked jar (no `@Metadata`) | JVM descriptors only | **no Kotlin types, no parameter names, no nullability.** §3.2 | | klib | **current:** `KlibScanner` builds `DeclarationModel` through `LibraryAbiReader` | what it cannot fill it declines on, and a declined declaration is not stubbed (`PythonStubsTask`); no klib declaration is bound at run time yet (SPEC B-2), so in practice no klib stub is emitted | @@ -307,7 +311,7 @@ is being fetched and then dropped, one function before it would be used. ## 3. Kotlin → Python type mapping -### 3.1 The table — the declared Kotlin type, a stub class per bound type +### 3.1 The table, the declared Kotlin type, a stub class per bound type **Current (issue #31).** A stub says what the Kotlin declaration says, wherever the runtime serves that type. `renderKotlinFqnStubs` (`PyiRendering.kt`) maps each `KotlinTypeModel` with its boundary @@ -363,14 +367,14 @@ nothing is emitted for that module (the runtime resolves the name to the functio A class in a walked jar with no `@Metadata` goes down `ArtifactScanner.javaStaticCandidates`, which has only the JVM descriptor. That path can produce `int`, `str`, `bytes`, `bool`, `float`, `None` and -nothing else — no nullability, and **no parameter names** (`MethodNode.parameters` is present only +nothing else, no nullability, and **no parameter names** (`MethodNode.parameters` is present only when the jar was compiled with `-parameters`, which is not the default and cannot be assumed). -Judged: for a Java-sourced declaration, emit positional-only parameters — +Judged: for a Java-sourced declaration, emit positional-only parameters, def assertEquals(__a0: int, __a1: int, /) -> None: ... -— rather than inventing names. A wrong keyword name is worse than no keyword name, because it +rather than inventing names. A wrong keyword name is worse than no keyword name, because it type-checks at the call site and fails at run time. Reference return types from Java get `| None`, because Java can return null and nothing in the bytecode says otherwise. @@ -422,7 +426,7 @@ expressible and are not faked. The Kotlin type stays in the docstring. - **The synthetic parameters are not stubbed.** Parameters come from `KmFunction` metadata, so `$composer`, `$changed` and `$default` never appear (`docs/design/kotlin-extensions-in-python.md` §4.6: metadata arity is the correct source). -- **A required parameter after a defaulted one is keyword-only**, exactly as `inspect.signature` on a +- **A required parameter after a defaulted one is keyword-only**: exactly as `inspect.signature` on a binder-made callable reports it (`KotlinSurface.kt`): `def Column(modifier: Modifier = ..., verticalArrangement: Any = ..., horizontalAlignment: Any = ..., *, content: Callable[[ColumnScope], None])`, and the defaults stay marked. The one exception is a declaration with an unwritable @@ -456,8 +460,10 @@ Python's one `int`/`float` and a class that fell back to `Any` cannot tell two K so each overload after the first carries `# type: ignore[overload-cannot-match]` (`TypedStubTest.everyOverloadAfterTheFirstSilencesTheCheckersNeverMatchedDiagnostic`). -Parameters are keyword-capable under their **Kotlin** names (`all`, `fraction`, `alignmentLine`); -nothing is converted to snake_case (`KotlinNamesOnlyStubTest.kotlinNamesAreNeverSnakeCased`). A name +Parameters are keyword-capable under the keyword `inspect.signature` shows: the Pythonic one where an +alias is served (`all`, `fraction`, `alignment_line`), else the Kotlin name; each module-level name also +gets `alias = kotlinName` (issue #131, SPEC U-12; +`KotlinNamesOnlyStubTest.aKotlinNameKeepsItsDefAndGainsItsPythonicAlias`). A name that is not a Python identifier or is a Python keyword, or an unknown name, forces positional-only up to and including that parameter, spelled `__a` (`KotlinNamesOnlyStubTest.aParameterNamedLikeAPythonKeywordForcesAPositionalOnlyPrefix`). A Kotlin @@ -472,7 +478,7 @@ default is `= ...`, because metadata carries `declaresDefaultValue` but not the | ordinary class or interface | 514 | method on the receiver's stub class | | Compose value class | 41 | method on the value class's stub | | Kotlin collection | 29 | module-level function | -| Kotlin primitive or `String` | 20 | module-level function — Python cannot attach a method to `int` | +| Kotlin primitive or `String` | 20 | module-level function, Python cannot attach a method to `int` | Nothing measured here disturbs that. Chaining is free because each of the 177 returns `Modifier`, and the mypy run in archive §4.4 confirms `Modifier.size(4).padding(2)` type-checks as `Modifier` in a Pythonic stub. @@ -510,22 +516,22 @@ attribute shape that replaced it are a Pythonic stub design: archive §4.2–§4 74 public member extensions on `Modifier` (`RowScope.weight`, `ColumnScope.align`, `BoxScope.matchParentSize`) need a dispatch receiver Kotlin supplies implicitly from the enclosing lambda. `docs/design/kotlin-extensions-in-python.md` §6 records that no shape has been chosen. Nothing here -changes that, and **they must not be stubbed as if they were plain methods on `Modifier`** — a stub +changes that, and **they must not be stubbed as if they were plain methods on `Modifier`**, a stub that says `Modifier.weight(1.0)` checks is a stub that promises a call the runtime cannot make. --- ## 5. Package mapping -There is none. A stub lives at the Kotlin module path the runtime publishes onto — +There is none. A stub lives at the Kotlin module path the runtime publishes onto, `androidx/compose/foundation/layout/__init__.pyi`, `junit/runner/Version/__init__.pyi` for a static on -a class — and nothing is emitted under `pythonx/` +a class, and nothing is emitted under `pythonx/` (`KotlinNamesOnlyStubTest.theStubLivesUnderTheKotlinModulePathAndNothingLivesUnderPythonx`, -`WalkedArtifactStubTest.theStubsUseKotlinNamesAndNothingIsExportedUnderPythonx`). The `androidx.` → +`WalkedArtifactStubTest.theStubsKeepKotlinNamesAddPythonicAliasesAndExportNothingUnderPythonx`). The `androidx.` → `pythonx.` default rule, the observation that `pythonx.compose.layout` wraps `androidx.compose.foundation.layout`, and the `pythonx-map.toml` manifest are in archive §5. -### 5.3 The Kotlin-FQN stubs — the one product +### 5.3 The Kotlin-FQN stubs, the one product Of the two products the original §5.3 named, only the first remains: @@ -550,7 +556,7 @@ Of the mypy runs in `/Volumes/macMini/tmp/pyicheck`, the two that bear on the Ko 3. **A stub-only tree with no `.py` and no `__init__.py` resolves.** A directory containing only `androidx/compose/material3/__init__.pyi` on `MYPYPATH` gave `Revealed type is "def (text: str, color: int =, fontSize: int =)"` and rejected `Text(3)`. So - the Kotlin-FQN stubs of §5.3 — which describe modules that exist only in `sys.modules` — need no + the Kotlin-FQN stubs of §5.3, which describe modules that exist only in `sys.modules`, need no accompanying files. 4. Namespace packages need no `__init__.py` anywhere above the stub; mypy's `--namespace-packages` is on by default. @@ -575,7 +581,7 @@ So: when chaquopy is applied; a `.pth` file in the venv for a desktop run; the interpreter path list for PyCharm. This is per-environment and none of it was verified. -**Current:** implemented as written for the first two bullets — `PythonStubsTask` writes +**Current:** implemented as written for the first two bullets, `PythonStubsTask` writes `build/generated/pythonStubs//`, deleting it first, and `generatePythonStubs` is a dependency of `prepareKotlinIdeaImport` (`PythonBindingsPlugin.configureStubGeneration`). No environment registration is implemented. A `pythonx` wheel's own stubs are pythonx-compose's (archive @@ -586,14 +592,14 @@ environment registration is implemented. A `pythonx` wheel's own stubs are pytho PyREPL's answer (§1.2) is the right shape: when the same qualified name is contributed by `commonMain` and by a platform source set, rename both and write an `__init__.pyi` that re-exports each. Its implementation splits on `"\\__init__.pyi"` and so does nothing outside Windows. The port -must use `File.separator` — or better, operate on `Path` objects and never on path strings, which is +must use `File.separator`, or better, operate on `Path` objects and never on path strings, which is what made the bug invisible. `docs/design/ecosystem.md` §5b's `JClass`/`KClass`/`ObjcClass` rule bears on this: a target that has no JVM has no `JClass` and says so. The stubs must reproduce that per source set rather than emitting a union that promises every platform's surface everywhere. -**Current:** not implemented — `PythonStubsTask` is registered per source set and nothing detects or +**Current:** not implemented, `PythonStubsTask` is registered per source set and nothing detects or merges an overlap. --- @@ -602,7 +608,7 @@ merges an overlap. - **A class whose name is a function in its module is `Any`** (§3.1): `PaddingValues`, `TextStyle`, `Color`, `AnnotatedString`, `Dp` in Compose 1.11.1. -- **Supertypes are the nearest one only**, and only for types a bound declaration returns (§3.1); an +- **Supertypes are the nearest one only**: and only for types a bound declaration returns (§3.1); an interface a class implements besides its first listed supertype is not in the stub. - **Type arguments of a non-function class are dropped** (§3.1). - **Suspend functions** (§3.5) are bound and awaitable but not stubbed. @@ -612,7 +618,7 @@ merges an overlap. accepts them, `TypeVar` is the mapping and nothing here covers it. - **klib.** The scanner produces declarations (§2.3), but none is bound at run time yet (SPEC B-2), so none is stubbed. -- **Member extensions** (§4.5) — 74 on `Modifier` — remain unstubbable until the scope-passing shape +- **Member extensions** (§4.5), 74 on `Modifier`, remain unstubbable until the scope-passing shape is chosen. - **IDE pick-up** (§6.2): registering `build/generated/pythonStubs//` with an interpreter or IDE is per-environment and none of it is implemented or verified; PyCharm was never tested @@ -621,10 +627,10 @@ merges an overlap. ## 8. Checked with mypy, and published -- `tools/stubs/check-stubs.sh ` runs `mypy --strict` over `tools/stubs/consumer.py` (a +- `.github/scripts/stubs/check-stubs.sh ` runs `mypy --strict` over `.github/scripts/stubs/consumer.py` (a `Modifier` chain from the class and from an instance, `Checkbox`, `Text`, `Column` with its keyword `content`, and four lines that must be rejected) against stubs generated for the real Compose jars - (`:ksp-fixtures:compose:generatePythonStubs`, which `ksp-fixtures/compose` now enables). Errors inside + (`:python-multiplatform-compose:fixtures:compose:generatePythonStubs`, which `python-multiplatform-compose/fixtures/compose` now enables). Errors inside the stubs are reported as well, not only in the consumer. - `.github/workflows/stubs.yml` runs the stub task and that check on every push to `develop`, uploads the stubs as the workflow artifact `kotlin-stubs` (with a README naming the Compose version and the diff --git a/docs/design/threading-and-abi.md b/docs/design/threading-and-abi.md index da806838e..22865f0cc 100644 --- a/docs/design/threading-and-abi.md +++ b/docs/design/threading-and-abi.md @@ -14,7 +14,7 @@ The library supports **two flavours**, chosen at build time: The interpreter version is pinned by `pythonVersion` in `gradle.properties` (3.14.7 at the time of writing). Free-threading is **not** gated on 3.15t: the whole desktop suite runs on 3.14.7t -(236 tests, 0 failures, 1 skipped — ROADMAP §9; SPEC T-2). Only desktop has free-threaded +(236 tests, 0 failures, 1 skipped, ROADMAP §9; SPEC T-2). Only desktop has free-threaded prebuilts; Android and iOS have none, so the opt-in is desktop-only. How the earlier "from 3.15t" conclusion was reached, and why it did not hold, is in [`../archive/threading-and-abi-3.15t-gate.md`](../archive/threading-and-abi-3.15t-gate.md). @@ -24,7 +24,7 @@ conclusion was reached, and why it did not hold, is in PEP 684 gives real parallelism on GIL builds by giving each sub-interpreter its own GIL. It was evaluated and rejected for this project. -It is technically reachable. `Py_NewInterpreterFromConfig` is exported from libpython — +It is technically reachable. `Py_NewInterpreterFromConfig` is exported from libpython, verified with `nm` on `libpython3.14.dylib`: ``` @@ -48,7 +48,7 @@ typedef struct { The Stable ABI is a compile-time contract, not a runtime one, and this project resolves symbols dynamically, so calling a non-limited symbol is possible. -The blocker is not the ABI — it is **extension compatibility**. Per-interpreter GIL requires every C +The blocker is not the ABI, it is **extension compatibility**. Per-interpreter GIL requires every C extension to implement multi-phase init and declare `Py_mod_multiple_interpreters`; extensions that do not simply fail to import. That would cost exactly the Python libraries this project exists to share, in return for parallelism. @@ -68,12 +68,12 @@ available on free-threaded builds for extension *modules*. What it asks of us if | Requirement | Our status | |---|---| | `PyObject` / `PyVarObject` become incomplete types; no field access | **Compatible.** `PyObject` appears only as an opaque pointer and is never dereferenced. | -| Extensions may not embed `PyObject` in their own structs | Compatible — nothing does. | +| Extensions may not embed `PyObject` in their own structs | Compatible, nothing does. | | `PyModExport` hook (PEP 793) instead of static `PyModuleDef` | Not relevant: we embed CPython rather than building an extension module. It would matter if Kotlin classes were ever exposed as a CPython extension module. | ## PEP 809 may collapse the two flavours into one (forward-looking) -[PEP 809](https://peps.python.org/pep-0809/) — **Draft**, also targeting 3.15 — would replace `abi3` +[PEP 809](https://peps.python.org/pep-0809/), **Draft**, also targeting 3.15, would replace `abi3` with time-bound versioned ABIs (`abi2026`), each frozen for at least ten years with at least five years of overlap. Crucially, a single extension compiled against `abi2026` supports **both** free-threaded and GIL-enabled builds. @@ -96,7 +96,7 @@ Desktop (macOS arm64/x86_64, Linux x86_64, Windows x86_64) has GIL and free-thre python-build-standalone releases; the build uses the `-freethreaded` archive when `pythonFreeThreaded=true`. **Android (python.org) and iOS (Python-Apple-support) publish no free-threaded build**, so free-threading stays a desktop-only opt-in, and enabling it by default -would split the threading model — and the object-lifetime and thread-state design layered on top — +would split the threading model, and the object-lifetime and thread-state design layered on top, across platforms. Switching the default would additionally need adequate free-threaded wheel coverage for the C extensions users care about. Table as of the 20260807 release, kept in the archive note above. diff --git a/docs/design/typedpython.md b/docs/design/typedpython.md index ff3bc4639..d2089d25e 100644 --- a/docs/design/typedpython.md +++ b/docs/design/typedpython.md @@ -1,4 +1,4 @@ -# TypedPython — 설계 제안 +# TypedPython, 설계 제안 > **참고 (2026-10-03):** §4.4 가 전제하는 스텁 매니페스트(`pythonx-map.toml`, 그리고 그것을 읽어 > `pythonx.*` 스텁을 내던 `StubManifest`/`renderPythonicStubs`)는 이 저장소의 스텁 생성기에서 @@ -11,7 +11,7 @@ > 구현하면서 달라질 수 있다. > **결정 (2026-10-02, 사용자):** TypedPython 은 별도 레포가 아니라 **PythonMultiplatform 의 기본 -> 동작**이다. 범위는 **"검사는 기본, 컴파일은 opt-in"** — 사용자 프로젝트의 파이썬 코드는 빌드할 때 +> 동작**이다. 범위는 **"검사는 기본, 컴파일은 opt-in"**, 사용자 프로젝트의 파이썬 코드는 빌드할 때 > 항상 정적 타입 검사를 거치고, 네이티브 컴파일은 표시한 코드에만 적용된다 (§2.3). --- @@ -31,7 +31,8 @@ TypedPython 의 목표는 **새 언어를 만들지 않고, 파이썬 문법 그 1. 타입이 확정된 코드는 **네이티브로 컴파일**해서 앱에 싣는다. PythonMultiplatform 의 배포 목표인 "파이썬 부분도 컴파일된 채로 들어간다" 와 같은 자리다. -2. GPU 커널을 **별도 언어(CUDA, Metal Shading Language, Mojo) 없이** 파이썬 서브셋으로 쓴다. +2. GPU 커널과 CPU SIMD 를 **별도 언어(CUDA, Metal Shading Language, Mojo) 없이** 파이썬 서브셋으로 + 쓴다. 2026-10-04 에 탐색에서 약속으로 올라갔다 (INTENT §1.5, §5). ## 2. 원칙 @@ -59,10 +60,10 @@ TypedPython 의 목표는 **새 언어를 만들지 않고, 파이썬 문법 그 | 등급 | 대상 / 표기 (안) | 보장 | 출력 | |---|---|---|---| -| 0 | 서드파티 패키지 (의존성) | 없음 — 검사하지 않는다 | 바이트코드 (pypackpack 이 받은 그대로) | -| 1 `checked` | **사용자 프로젝트의 파이썬 코드 전부 — 기본값** | 빌드 타임 타입 오류, `Any` 유출 0 (§4.2) | 바이트코드 | -| 1 `compiled` | `@compiled` (내장 이름, import 없음) 또는 모듈 선언 `# typedpython: compiled` | `checked` + §3.1 금지 목록 | C 확장 모듈 | -| 2 `kernel` | `@kernel` | 객체·힙 할당 없음, 고정 dtype | SPIR-V / MSL / WGSL / PTX | +| 0 | 서드파티 패키지 (의존성) | 없음, 검사하지 않는다 | 바이트코드 (pypackpack 이 받은 그대로) | +| 1 `checked` | **사용자 프로젝트의 파이썬 코드 전부, 기본값** | 빌드 타임 타입 오류, `Any` 유출 0 (§4.2) | 바이트코드 | +| 1 `compiled` | `@compiled` (내장 이름, import 없음) 또는 모듈 선언 `# typedpython: compiled` | `checked` + §3.1 금지 목록 | C 확장 모듈 (NEON · AVX 벡터화, §5.3) | +| 2 `kernel` | `@kernel` | 객체·힙 할당 없음, 고정 dtype | SPIR-V / MSL (직접) / WGSL / PTX, 빌드 타임 (§5) | - **사용자 코드는 표시하지 않아도 검사된다.** 타입 오류가 있으면 Kotlin 컴파일 에러처럼 빌드가 실패한다. TS 의 `strict` 기본값과 같은 위치다. @@ -73,13 +74,13 @@ TypedPython 의 목표는 **새 언어를 만들지 않고, 파이썬 문법 그 §3.1 금지 목록을 지키는 것뿐이다. - 등급 사이의 경계에서는 **검사된 변환**이 일어난다. -## 3. 등급 1 — checked / compiled +## 3. 등급 1, checked / compiled ### 3.1 금지 목록 -`compiled` 코드에서는 다음을 컴파일 에러로 다룬다. `checked` 코드에서는 경고로 보고한다 — 기본 검사가 +`compiled` 코드에서는 다음을 컴파일 에러로 다룬다. `checked` 코드에서는 경고로 보고한다, 기본 검사가 기존 파이썬 관용구를 막아 채택을 해치지 않게 하면서, `@compiled` 로 옮기기 전에 무엇을 고쳐야 하는지 -미리 보이게 하려는 것이다. (이 경고/에러 구분은 제안이다 — §8.) +미리 보이게 하려는 것이다. (이 경고/에러 구분은 제안이다, §8.) - 변수의 타입 재할당 (`x = 1` 뒤 `x = "a"`) - `eval` / `exec`, 이름을 문자열로 만드는 `getattr`/`setattr` @@ -97,7 +98,7 @@ TypedPython 의 목표는 **새 언어를 만들지 않고, 파이썬 문법 그 ## 4. 등급 1 의 구현 -### 4.1 프론트엔드 — Pyrefly +### 4.1 프론트엔드, Pyrefly Meta 의 Pyrefly 를 타입 추론기로 쓴다. @@ -108,7 +109,7 @@ Meta 의 Pyrefly 를 타입 추론기로 쓴다. **포크하지 않는다.** 처음에는 CLI 와 기계 판독 가능한 출력 수준에서 붙이고, 라이브러리 임베드는 API 안정성을 확인한 뒤로 미룬다. -### 4.2 건전성 게이트 — TypedPython 고유의 일 +### 4.2 건전성 게이트, TypedPython 고유의 일 타입 체커와 컴파일러는 목표가 반대다. @@ -125,7 +126,7 @@ API 안정성을 확인한 뒤로 미룬다. **이 게이트만으로도 독립적인 가치가 있다.** 컴파일러가 없어도 "사용자 코드에는 `Any` 가 없다" 는 보장이 생기고, 그것이 AI 친화적 피드백 루프의 핵심이다. 이후 모든 단계가 이 게이트를 전제로 한다. -### 4.3 백엔드 — 자체 typed IR → C (결정 2026-10-03, #41) +### 4.3 백엔드, 자체 typed IR → C (결정 2026-10-03, #41) **Cython 은 쓰지 않는다** (사용자: "Cython 의존 별로야. 2단계로 바로 진행하도록 해"). Cython 경로 (PR #39)는 닫았고, 거기서 검증된 의미 보존 규칙만 IR 계약으로 옮겼다. @@ -136,7 +137,7 @@ API 안정성을 확인한 뒤로 미룬다. - **IR 이 계약이다** (`typedpython/ir.py`). 각 노드는 CPython 동작으로 정의된다: 검사되는 i64 산술과 오버플로 시 deopt(인터프리터 재실행), 파이썬식 floor 나눗셈·나머지, 정확한 int/float 비교, 리스트식 인덱싱, `math` 모듈의 예외. C 런타임 API 는 `typedpython/runtime/API.md` 에 고정했다. -- **왜 C 인가, LLVM 이 아니라.** 모든 타깃에 C 컴파일러가 이미 있다(clang, NDK, Xcode) — 새 의존이 없다. +- **왜 C 인가, LLVM 이 아니라.** 모든 타깃에 C 컴파일러가 이미 있다(clang, NDK, Xcode), 새 의존이 없다. CPython C API 가 C 이고, 플랫폼 디버거가 그대로 쓰인다. 타입이 확정된 스칼라·배열 코드에서 중요한 최적화(인라이닝, 벡터화)는 C 컴파일러가 이미 한다. IR 이 안정 계층이라 LLVM 백엔드는 나중에도 붙일 수 있다. - **왜 Rust 가 아닌가.** 파이썬의 별칭과 공유 변경은 Rust 소유권에 맞지 않아 생성 코드가 @@ -148,7 +149,7 @@ API 안정성을 확인한 뒤로 미룬다. ### 4.3.1 안전성은 IR 에서 증명한다 (SPEC N-9) -사용자 결정: "IR 단계에서 안전성을 증명하도록 해". C 를 만들기 전에 `verify.py` 가 IR 에서 증명한다 — +사용자 결정: "IR 단계에서 안전성을 증명하도록 해". C 를 만들기 전에 `verify.py` 가 IR 에서 증명한다, 정의 전 사용 없음, 노드 타입 일치, 비순수 함수에 deopt 노드 없음, 배열 접근은 경계 증명 또는 런타임 검사 유지, 배열 파라미터는 인덱싱·저장·len 만, OBJ 참조는 하나의 소유 규칙. **증명하지 못한 함수는 컴파일하지 않고** 인터프리터에 남기며 이유를 진단으로 낸다. 생성 C 는 `tp_runtime.h` 헬퍼로만 메모리와 `PyObject` @@ -163,31 +164,32 @@ Gradle 태스크는 입력·출력을 정확히 선언해 up-to-date 판정과 ### 4.3.3 고정 레이아웃 클래스 (SPEC N-11, M2b) -**레이아웃이 CPython 에서도 고정일 때만** 고정 접근으로 컴파일한다 — 사용자가 `__slots__` 를 직접 쓴 클래스. +**레이아웃이 CPython 에서도 고정일 때만** 고정 접근으로 컴파일한다, 사용자가 `__slots__` 를 직접 쓴 클래스. 컴파일러가 `__slots__` 를 몰래 더하면 `__dict__`·`vars()`·속성 추가가 CPython 과 달라지므로("같은 결과" 원칙 위반) 하지 않는다. binary-trees 벤치의 `Node` 는 그래서 `__slots__ = ("left", "right")` 를 소스에 적어야 컴파일되고, 그 변경은 모든 언어 열에 공평하게 적용되도록 벤치 소스 자체에 넣는다(CPython 기준선 재측정 필요). - **필드 접근:** `type(x) is C` 로 가드된 값의 필드는 member descriptor 의 오프셋으로 바로 읽고 쓴다. 오프셋은 모듈 초기화 때 클래스 사전의 descriptor 가 정말 member descriptor 인지 확인하고 잡는다. 값은 객체 그대로다. -- **순수성:** 슬롯 읽기는 사용자 코드를 실행하지 않으므로 효과가 아니다 — `check(node)` 같은 재귀 순회가 +- **순수성:** 슬롯 읽기는 사용자 코드를 실행하지 않으므로 효과가 아니다, `check(node)` 같은 재귀 순회가 순수 함수로 남아 i64 경로를 쓸 수 있다. - **생성:** `__init__` 이 매개변수를 각 슬롯에 한 번씩 대입하기만 하면 `C(a, b)` 를 할당 + 슬롯 채우기로 내린다. 그 밖의 `__init__` 은 객체 호출로 남는다. -- **메서드:** `self` 를 `type(self) is C` 로 가드한 함수로 컴파일한다. -- **IR 확장(예정):** `FieldGet(obj, cls, field)`, `FieldSet(obj, cls, field, value)`, `New(cls, args)`, - 그리고 정확한 타입 가드 `IsExact(obj, cls)` 조건. 검증기는 가드 없는 필드 접근, 슬롯 밖 필드, 생성자 규칙 - 위반을 거부한다. +- **메서드(예정):** `self` 를 `type(self) is C` 로 가드한 함수로 컴파일한다. 아직 구현 전이다. +- **IR (구현됨, #124):** `FieldGet`, `FieldSet`, `New`, `IsExact`, `CheckExact`, `Is`, `ClassDecl`, `Param.cls`. + 검증기는 정확한 타입 증명이 없는 필드 접근, 슬롯 밖 필드, 생성자 규칙 위반을 거부한다. +- **초기화 뒤 변경 (구현됨):** 슬롯 접근마다 정확한 타입과 클래스의 `tp_version_tag` 를, 생성마다 전역 바인딩을 + 확인하고, 다르면 일반 경로(비순수) 또는 deopt(순수)로 간다. 세부는 SPEC N-11. 검토하고 기각한 경로: -- **Cython** — 사용자 결정으로 폐기(위). -- **mypyc** — 타입 체커가 mypy 에 묶여 있어 Pyrefly 와 판정이 갈라진다. -- **Nuitka** — 동작 동일성을 우선해 타입 특수화를 하지 않는다. -- **LPython** — 업스트림 활동이 LFortran 쪽으로 옮겨간 것으로 보인다. -- **Rust 생성** — 위. +- **Cython**: 사용자 결정으로 폐기(위). +- **mypyc**: 타입 체커가 mypy 에 묶여 있어 Pyrefly 와 판정이 갈라진다. +- **Nuitka**: 동작 동일성을 우선해 타입 특수화를 하지 않는다. +- **LPython**: 업스트림 활동이 LFortran 쪽으로 옮겨간 것으로 보인다. +- **Rust 생성**: 위. -### 4.3.3 재귀 (SPEC N-8, #57) +### 4.3.3a 재귀 (SPEC N-8, #57) 호출 사이클(직접·상호 재귀)은 컴파일한다. 컴파일된 프레임에는 파이썬 프레임이 없으므로 모든 컴파일 함수의 진입에서 `tp_enter_call()`, 모든 출구(ok·error·deopt)에서 `tp_leave_call()` 을 부른다. 진입은 @@ -197,12 +199,194 @@ Gradle 태스크는 입력·출력을 정확히 선언해 up-to-date 판정과 지킨다. **측정(3.13.0, 3.14.7, 기본 한도 1000):** 인터프리터와 컴파일 재귀 모두 같은 깊이(997)에서 실패하고, 인터프리터 프레임 k 개 위에서 컴파일 재귀를 시작해도 `최대 n + (추가 프레임 수)` 가 일정해 오프셋이 없다. 오프셋이 생기는 곳은 둘이다. (1) 한도를 C 스택이 먼저 닫을 만큼 올리면(예: 100만) C 가드의 -메시지가 나온다 — 3.13 은 `maximum recursion depth exceeded in compiled code`, 3.14 는 -`Stack overflow (used 8144 kB) in compiled code` — 인터프리터는 같은 한도에서 파이썬 한도 메시지를 낸다. +메시지가 나온다, 3.13 은 `maximum recursion depth exceeded in compiled code`, 3.14 는 +`Stack overflow (used 8144 kB) in compiled code`, 인터프리터는 같은 한도에서 파이썬 한도 메시지를 낸다. (2) 카운터는 모듈별(헤더가 `static`)이라, 한 컴파일 모듈의 두 프레임 사이에 **다른** 컴파일 모듈의 프레임이 끼면 그 프레임은 세지 않는다. 테스트는 두 경우 모두 "둘 다 예외를 내고 죽지 않는다" 만 단언하고 깊이를 기록한다(`test_recursion.py`). +### 4.3.4 객체 비용: 가상 객체, refcount 생략, 영역 할당 (SPEC N-12, 설계) + +사용자 지시 (2026-10-03): "Rust 쪽으로 더 가는 방법을 적용하되 cpython과 동일성을 보장할 방법을 찾아." + +**왜 필요한가.** 숫자·배열 코드는 이미 Rust 근처다(nbody 31x, Rust 의 약 2배). 객체가 많은 코드는 그렇지 +않다. binary-trees 는 1.7x 인데, 컴파일된 코드도 노드마다 CPython 할당(`tp_alloc`: 0 채우기, GC 헤더, +GC 추적), refcount 증감, 해제(`subtype_dealloc`)를 그대로 치르기 때문이다(#125, 아직 프로파일로 확정 전). +더 가려면 객체를 아예 만들지 않거나(가상 객체), 증감을 빼거나(refcount 생략), 한꺼번에 할당·해제해야 +한다(영역 할당). 셋 다 `id()`, `__del__` 시점, `gc` 관측과 부딪칠 수 있다. + +**동일성의 정의.** 컴파일된 코드의 모든 실행은 **어떤 유효한 CPython 실행과 구별할 수 없어야 한다.** +구별은 관측으로만 할 수 있으므로, 설계는 "관측 지점" 목록과 "관측 지점에 닿기 전에 실제 객체로 되돌린다"는 +규칙으로 이루어진다. CPython 자체가 정하지 않는 것(스레드 전환 시점)은 CPython 이 고를 수 있는 실행 중 +하나를 고른 것으로 본다. 컴파일된 코드는 GIL 을 놓지 않으므로, 컴파일 구간 안에서 다른 스레드가 끼어드는 +실행은 생기지 않는다. 이는 CPython 이 그 구간에서 전환하지 않은 실행과 같다. + +> **free-threaded 3.15t 에서 다시 볼 점 (#193, #159).** 위 문장은 GIL 빌드의 논증이다. 3.15t 에는 GIL 이 없어 다른 +> 스레드가 컴파일 구간과 동시에 돈다. 그래서 다른 스레드에서 보일 수 있는 객체에 대한 아래의 최적화(특히 나의 +> refcount 생략)는 스레드 지역으로 증명된 객체로 제한한다. 가상 객체는 정의상 실체화 전까지 다른 스레드가 볼 수 +> 없으므로 영향이 없다. + +#### 가. 가상 객체 (부분 탈출 분석 + 지연 실체화) + +PyPy·GraalVM 과 같은 방식이다. N-11 클래스의 객체를 PyObject 대신 C 구조체(필드 값만)로 두고, 관측 +지점에 닿기 직전에 실제 PyObject 로 만든다(실체화). 실체화된 뒤로는 인터프리터와 같은 객체이므로 결과가 같다. + +- **대상 클래스.** N-11 조건에 더해 `__del__` 이 없고 `__slots__` 에 `__weakref__` 가 없는 클래스만. 이 둘이 + 있으면 객체의 죽는 시점이 사용자 코드로 관측된다. `base = object` 이고 슬롯이 고정이므로 weakref 는 + `__weakref__` 슬롯으로만 생긴다. +- **관측 지점(실체화 트리거).** 아래에 닿는 가상 객체는 그 직전에 실체화한다. + + | 관측 | 이유 | + |---|---| + | 함수 밖으로 반환, 전역·컨테이너·실객체 필드에 저장 | 컴파일 구간 밖에서 보인다 | + | 미지 코드 호출의 인자, `id()`, 기본 `hash()`·`repr()` | 객체 자체나 주소가 드러난다 | + | **살아 있는 동안 일어나는 모든 미지 코드 호출** | 인자로 넘기지 않아도 `gc.get_objects()`·`gc.get_referrers()`·`tracemalloc` 이 존재와 메모리를 본다. 그래서 그 시점에 살아 있는 가상 객체는 **전부** 실체화한다 | + | deopt | 순수 함수의 deopt 는 인자부터 인터프리터로 다시 실행하므로 내부 가상 객체는 버리면 된다. 비순수 함수는 효과 뒤 deopt 가 없다(N-9) | + + `is`, `type(x)`, `isinstance` 는 관측 지점이 아니다. 두 가상 객체의 `is` 는 할당 지점 동일성으로 정적으로 + 정해지고, 가상 객체와 실객체는 같을 수 없다. 타입은 정적으로 안다. +- **실체화 순서.** 할당 순서대로 만든다. `gc.get_objects()` 의 순서도 관측되기 때문이다. 실체화는 도달 + 가능한 가상 그래프 전체를 만들고 필드를 채운 뒤 진행한다. +- **효과.** 가상 객체는 미지 코드 호출이 없는 구간에서만 산다. 트리를 만들고 바로 소비하는 코드(binary-trees + 의 `check(make(d))`)는 그런 구간이다. `print` 를 섞는 루프는 매 반복 실체화하므로 지금과 같다. + +#### 나. refcount 생략 + +- 같은 객체의 증가·감소 쌍을 빼는 것은 **그 사이 내내 다른 소유 참조가 그 객체를 살려 둔다**는 것과 + **그 사이에 미지 코드 호출이 없다**는 것을 IR 에서 증명할 때만 한다. 감소가 0 에 닿을 수 있는 지점은 + 해제 지점이므로 빼지 않는다. +- 첫 대상은 빌린 읽기다. 예를 들어 `check(node.left)` 는 지금 `FieldGet` 이 새 참조를 만들어 넘기고 돌아와 + 놓는다. 바깥 객체가 소유되어 있고, 그 슬롯이 그동안 쓰이지 않고, 사용자 코드가 돌지 않으면 빌린 참조로 + 넘길 수 있다. +- `sys.getrefcount` 는 호출로만 관측되고, 호출은 위 조건으로 막혀 있으므로 관측되는 refcount 는 늘 정확하다. + **free-threaded 3.15t 에서는 이 논증이 성립하지 않는다(#193, 결정 D5).** GIL 이 없으므로 다른 스레드가 그 + 사이에 슬롯을 쓰고 자식을 해제할 수 있다. 그래서 빌린 읽기와 refcount 생략은 **스레드 지역으로 증명된 객체에만** + 적용하고, 나머지는 실제 참조를 잡거나 CPython 의 객체별 critical section 안에서 읽는다 (§4.3.5). + +#### 다. 영역(arena) 할당 + +- 개수 상한이 있는 가상 객체는 C 스택에 둔다. 재귀 구조처럼 개수에 상한이 없는 가상 객체(`make(d)` 의 트리)는 + **활성(activation) 하나에 묶인 영역**에 할당하고, 그 활성이 끝날 때 한꺼번에 놓는다. +- **실객체는 영역에 두지 않는다.** CPython 이 실객체를 하나씩 해제하기 때문이다(`tp_free`). 영역에는 가상 + 객체만 있고, 실체화는 영역 밖에 실객체를 새로 만든다. +- **수명.** 영역은 수명을 감싸는 가장 안쪽 범위(루프 한 반복 등)에 둔다. CPython 이 일찍 놓는 객체를 영역이 + 함수 끝까지 붙잡으면 최대 메모리가 커진다. 그 차이는 `tracemalloc` 같은 관측에서 드러날 수 있지만, + 관측은 미지 코드 호출로만 일어나고 그 직전에 실체화하므로, 남은 차이는 프로세스 메모리 총량뿐이다. +- **할당 실패.** 영역 할당이 실패하면 CPython 이 그 할당에서 냈을 `MemoryError` 를 같은 지점에서 낸다. +- **함수 경계.** `make` 가 만든 트리를 호출자가 바로 소비하면, 호출자의 영역에 쓰는 `make` 의 변형을 따로 + 만든다. 할당 지점 특수화이고, 공개 진입점(wrapper)은 늘 실체화한 객체를 돌려준다. + +#### 라. 증명과 시험 + +- **증명(N-9 확장).** 검증기가 증명하는 것은 넷이다. 가상 객체가 영역·스택 밖으로 새지 않는다. 모든 관측 + 지점 앞에 실체화가 있다. 생략한 refcount 쌍마다 살려 두는 참조가 있다. 영역 수명이 그 안의 모든 참조 + 수명을 감싼다. 증명하지 못하면 N-11 경로(실객체)로 컴파일한다. +- **차등 시험.** 관측 연산을 일부러 넣은 경우를 인터프리터와 비교한다. 대상은 다음과 같다. + - 가상 객체에 대한 `id()`, 기본 `hash`·`repr`. + - 살아 있는 동안 부른 함수 안에서의 `gc.get_objects()` 수·순서, `gc.get_referrers`. + - 호출 경계의 `sys.getrefcount`, `tracemalloc` 스냅샷. + - 가상 객체와 실객체 사이의 `is`. + - 생성 도중 `MemoryError`. 시험용 할당기 훅으로 일으킨다. + - `__del__`·`__weakref__` 가 있는 클래스. 대상에서 빠지는지만 본다. +- **변형 시험.** 미지 호출 앞 실체화를 빼면 `gc.get_objects` 시험이 실패해야 한다. refcount 쌍의 살려 두는 + 참조 조건을 빼면 해제 시점 시험이 실패해야 한다. + +#### 마. 동일성을 지키려면 결정이 필요한 점 (사용자 결정, 리더 경유) + +1. **GC 수집 시점.** CPython 3.12 이후 세대 수집은 GC 추적 객체 할당 수로 예약되고, 다음 eval breaker 검사에서 + 실행된다. 가상 객체는 할당되지 않으므로 수집이 늦어지거나 빠진다. 그러면 **다른 곳의** 순환 쓰레기의 + `__del__`, weakref 콜백, `gc.callbacks` 가 실행되는 시점이 옮겨간다. + - (a) 지키는 방법: 가상 객체를 만들 때마다 GC 할당 카운터를 실제 할당과 같이 올리고, 수집이 예약되면 그 + 자리에서 실행한다. 둘 다 CPython 내부 구조체(비공개 API)를 쓴다. 임베디드 CPython 판이 고정이라 가능은 + 하지만 판이 바뀔 때마다 다시 확인해야 한다. + - (b) 범위 밖으로 두는 방법: 언어 레퍼런스는 수집 시점을 구현이 정하도록 허용하고(데이터 모델 §3.1), + CPython 도 3.13→3.14 에서 증분 GC 로 바꾸며 시점이 달라졌다. + - **결정 (2026-10-04, 사용자): (b).** GC 수집 시점은 동일성 범위에서 뺀다. 그 밖의 모든 관측은 정확히 지킨다. +2. **이미 있는 차이: eval breaker 를 검사하지 않는다(N-8 부터).** 컴파일된 코드는 루프 안에서 eval breaker 를 + 보지 않는다. 그래서 긴 컴파일 루프 동안 넷이 미뤄진다: 시그널(`KeyboardInterrupt`), 예약된 GC, pending + call, **다른 스레드로의 GIL 넘김.** 앞의 셋은 시점 차이지만, 마지막은 다른 스레드가 굶는 동작 차이다. + 루프 뒤 간선(back-edge)과 호출에서 eval breaker 를 검사하면 고쳐진다. + **1단계 구현 (#141):** 루프와 함수 진입에서 폴링하고, 일정 횟수마다 모듈의 `lambda: None` 을 불러 CPython 자기 + eval breaker 처리를 그대로 돌린다. 배열 copy-in·전역 스냅샷을 쥔 함수가 실행 중이면 미룬다. 그 함수 안에서의 + GIL 넘기기는 2단계(#154)이고, 방식은 결정 대기(#152). + **비용 (2026-10-17, 조용한 창, develop 대비 교대 11쌍):** fannkuch +8.2% (중앙값 1.082, 범위 1.043~1.191), + nbody 0.995 (배열 스냅샷 함수는 폴링하지 않음). 루프 폴링을 뺀 실험판은 fannkuch 1.000 이므로 남은 비용은 + 전부 짧은 안쪽 루프의 매 반복 카운트다운이다. CPython 도 모든 back-edge 에서 eval breaker 를 검사하므로 같은 + 종류의 비용이고, 시그널·스레드 굶김과 3.15t 의 GC stop-the-world 차단을 고치는 대가로 받아들였다(리더 결정). + range 루프의 묶음 폴링은 후속 이슈다. 원자료: `benchmarks/typedpython/results/2026-10-17-eval-breaker-*.json`. +3. **이미 있는 차이: 추적·모니터링.** 컴파일된 함수는 `sys.settrace`·`sys.setprofile`·`sys.monitoring` + 이벤트를 내지 않는다. 진입에서 추적이 켜져 있으면 인터프리터로 실행하는 방법(deopt 경로 재사용)을 제안한다. + 이것도 별도 이슈다. + +이 셋은 N-12 이전부터 있는 경계도 포함한다. 1번이 결정되어 N-12 구현을 시작한다. 2번은 #141(1단계 완료), 3번은 #142. + +### 4.3.5 소유권 추론, 메모리, 인터프리터 최소 상주 (설계, 결정 2026-10-05) + +사용자 질문 (2026-10-05): "GC의 동작을 smart pointer로 전환하는 컴파일러를 개발하는게 가능할까?", 그리고 +CPython 의 두 약점(Rust 대비 연산 속도와 메모리 사용) 중 메모리가 지금 설계로 얼마나 풀리는가. 근거와 측정은 +[`typedpython-ownership-memory.md`](../investigations/typedpython-ownership-memory.md). + +**전제.** CPython 객체는 이미 객체 안에 refcount 를 둔 smart pointer 이고, GC 는 순환만 맡는다. 그래서 "smart +pointer 로 전환" 의 실제 뜻은 컴파일 타임에 소유권을 추론해 refcount 연산과 GC 부담을 없애는 것이다. GC 를 +통째로 없애지는 못한다. 인터프리터와 서드파티 코드가 같은 힙을 쓰고, 사용자가 순환 구조를 만들 수 있다. + +#### 가. 소유권 추론 (관측 차이 없음, 결정 불필요) + +§4.3.4 의 가상 객체(고유 소유), refcount 생략(빌림), 영역 할당(영역 소유)을 함수 경계 너머로 넓힌다. + +- 컴파일된 함수의 매개변수와 반환값에 **소유 이전 / 빌림** 을 추론해 붙인다. 선례는 Lean 4 의 borrow 추론, + Koka Perceus, Lobster(런타임 refcount 연산 약 95% 제거 보고), Swift OSSA 다. 이들은 불변성이나 단일 소유자 + 가정에서 이득을 얻는데, 파이썬에는 그 가정이 없으므로 이득은 증명되는 범위에 한정된다. +- **free-threaded 에서 효과가 크다.** 3.15t 에서 refcount 증감 한 쌍은 현재 스레드 소유 객체에 약 3.2 ns, 다른 + 스레드가 만든 객체에 약 14.2 ns 다(실측). 다만 빌린 참조는 **스레드 지역으로 증명된 객체에만** 쓴다(#193). +- Perceus 식 제자리 재사용(refcount 1 이면 해제 대신 재사용)은 `gc.get_objects` 순서와 `tracemalloc` 에서 + 관측되고, 유일 참조 판정 API 가 stable ABI 밖이라 지금은 하지 않는다. + +#### 나. 메모리: 무엇이 줄고 무엇이 남는가 + +실측(Rust 대비, 3.15t): float 40 B 대 8 B, int 44 B 대 8 B, 실수 필드 2 개 객체 약 157 B 대 16 B, 정수 리스트 +원소당 56 B 대 8 B, 빈 인터프리터 실점유 약 7.6 MB 대 Rust 빈 프로그램 RSS 1.4 MB. 지금 설계(가상 객체, 영역, +i64/f64 지역)는 **컴파일된 구간 안의 임시 값**만 줄인다. 컨테이너·전역에 사는 오래 가는 데이터와 인터프리터 +바닥은 그대로다. + +결정에 따라 다음을 한다. + +- **스칼라 슬롯 직접 저장 (D1, D2, D3).** `@compiled` 클래스에서 `int`·`float` 로 선언된 슬롯은 객체 포인터 + 대신 i64/f64 값을 객체 안에 저장한다(객체당 약 3.3 배 감소, 실측 근거는 조사 문서). 인터프리터가 읽을 때마다 + 새 int/float 를 만든다. + - **D1 (예):** 슬롯에서 읽은 정확한 `int`·`float` 값의 동일성(`is`, `id`)은 동일성 보장에서 뺀다. 언어 규격이 + 불변 값의 동일성을 구현에 맡기므로 "어떤 유효한 구현과 구별할 수 없다" 는 원칙 안이다. 서브클래스 값은 대상이 + 아니다. + - **D2 (예, 조건부):** 이 클래스의 타입은 컴파일러가 `PyType_FromSpec` 으로 만든다. `__module__`, + `__qualname__`, `__slots__`, MRO, pickle, `inspect` 결과가 원래 `class` 문과 같음을 차등 시험으로 보인다. + - **D3 (예, 조건부):** 이 클래스의 슬롯에 넣는 값은 선언 타입을 강제한다(그 자리에서 `TypeError`). 사용자 + 코드는 이미 게이트를 통과하므로 막히는 것은 타입을 어긴 외부 동적 코드다. opt-in 이 곧 동의다. +- **GC 없는 클래스 (D4, 아니오).** GC 헤더 제거는 GIL 빌드에서 객체당 16 B 를 줄이지만 3.15t 에서는 0 B 다(실측). + 유일한 대상이 3.15t 이므로 하지 않고, 그래서 상속 금지(final)도 두지 않는다. +- **타입 확정 컨테이너.** 압축 배열은 `array.array` 나 별도 opt-in 타입으로 안내한다. `list` 의 표현은 바꾸지 + 않는다(동일성 위험이 가장 크다). +- **기대치.** 타입을 확정한 데이터는 Rust 의 약 1~2 배, `dict`·`str`·일반 클래스 위주의 코드는 지금 수준(3~10 배) + 이 크게 줄지 않는다. 이 문단은 추정이고, 벤치마크에 실점유 메모리 측정을 더해 기준선으로 박는다. + +#### 다. 인터프리터 최소 상주 + +인터프리터는 갱신 가능한 코드(FastTrack), 서드파티 동적 코드, 개발 중 핫 리로드를 맡고, 그 밖에서는 바닥만 +쓴다. 바닥(실점유 GIL 5.7 MB, 3.15t 7.6 MB)은 CPython 런타임이 정하고, 앱이 정하는 것은 import 다(흔한 모듈 다섯 +개에 10~12 MB). + +- **PEP 810 `lazy import` (결정 불필요).** 3.15 에 들어갔다. 쓰지 않는 import 다섯 개를 lazy 로 두면 3.15t RSS 가 + 30.3 MB 에서 17.5 MB 로 줄었다(실측). 컴파일러가 몰래 넣지 않는다(import 시점과 부작용이 바뀐다). pythonx + 라이브러리와 생성 코드에서 적극적으로 쓴다. +- **D6 (예, 앱 배포 빌드만):** `site` 를 끈다(약 1 MB). pypackpack 이 `sys.path` 를 정하므로 할 일이 없다. + `.pth` 처리와 `quit`/`exit` 이 사라지는 점을 문서화하고, 개발 모드는 켜 둔다. +- **D7 (예):** free-threaded 빌드는 해제한 메모리를 OS 에 돌려주지 않았다(객체 100 만 개 해제 뒤 점유 그대로, + 실측). 우리가 직접 빌드하는 FT CPython 에 플랫폼 메모리 압박(Android `onTrimMemory`, iOS 메모리 경고) 때 + mimalloc 정리를 부르는 최소 패치를 넣고 upstream 에도 제안한다. +- **D8 (지금은 아니오):** 클래스·함수 정의를 C 초기화 코드로 바꿔 인터프리터 의존을 줄이는 설계는 규모와 위험이 + 크고, 같은 문제를 lazy import 가 더 싸게 줄인다. +- 효과가 작아 하지 않는 것: `-OO`(0.35~0.8 MB, `assert` 도 사라짐), deopt 원본 지연 로딩(함수 1000 개당 약 + 0.4 MB), 동결 모듈 끄기(오히려 4.4 MB 증가). + ### 4.4 Kotlin 경계의 직접 호출 이 플랫폼에서만 가능한 이점이다. @@ -218,43 +402,183 @@ Gradle 태스크는 입력·출력을 정확히 선언해 up-to-date 판정과 **선행 조건:** 스텁 생성기가 픽스처 대역이 아닌 실제 `pythonx-map.toml` 을 읽고, 생성된 스텁이 IDE·타입 체커가 찾는 자리(pythonx-compose 배포본)에 설치돼야 한다. 2026-10 현재 미완료. -## 5. 등급 2 — 별도 언어 없는 GPU +## 5. 등급 2, 별도 언어 없는 커널 + +> **결정 (2026-10-04, 메인테이너):** GPU 커널과 CPU SIMD 는 탐색이 아니라 **약속**이다 (INTENT §1.5). +> 아래 백엔드 결정의 근거는 [`typedpython-kernel-backend.md`](../investigations/typedpython-kernel-backend.md) +> 와 [`typedpython-kernel-ir.md`](../investigations/typedpython-kernel-ir.md) 에 있다. 두 조사는 실제 기기 +> 성능을 재지 않았다. 아래 "측정" 표기가 붙은 항목은 첫 조각에서 확인한다. ### 5.1 하드 제약: AOT **iOS 는 런타임 코드 생성을 금지한다.** torchnative `docs/design/DESIGN.md` 는 이 이유로 Triton 을 기각했다. Android 에도 인앱 GPU 컴파일러 툴체인이 없고, autotuning 워밍업은 짧은 세션에서 상각되지 -않는다. 따라서 **GPU 커널은 빌드 타임에 컴파일된다.** JIT 은 데스크톱 개발 편의로만 허용한다. +않는다. 따라서 **커널은 빌드 타임에 컴파일된다.** JIT 은 데스크톱 개발 편의로만 허용한다. 컴파일러는 +빌드 도구에만 들어가고 앱에는 컴파일된 커널만 실린다. 그래서 컴파일러 쪽 의존성의 무게는 앱 크기가 +아니라 툴체인의 문제다. -### 5.2 커널 모델 +### 5.2 커널 모델: 타일 우선, SIMT 는 폴백 -Triton · NVIDIA Warp 처럼 파이썬 함수에 `@kernel` 을 붙인다. +파이썬 함수에 `@kernel` 을 붙인다. -- **처음에는 SIMT** (스레드 인덱스 기반). 컴퓨트 셰이더에 그대로 대응된다. -- 타일 추상화(Triton · cuTile 식)는 그다음. -- 커널 안: 고정 dtype 스칼라, 버퍼·텐서 뷰, 산술, 제어 흐름, 공유 메모리, 배리어. +- **기본은 타일 수준이다.** 타일 복사, 타일 행렬곱, 단계별 파이프라이닝, 공유 메모리·레지스터 조각 + 할당을 1급 연산으로 둔다 (TileLang 의 `T.copy` · `T.gemm` · `T.Pipelined` 와 같은 층위). AI 커널의 + 성능은 행렬 유닛(텐서 코어, Apple simdgroup_matrix · Metal 4 cooperative tensor, Vulkan + `VK_KHR_cooperative_matrix`)과 메모리 계층을 쓰는 데서 나오고, 그것을 표현하는 단위가 타일이다. +- **SIMT(스레드 인덱스 기반)는 폴백이자 저수준 탈출구다.** 행렬 유닛이 없는 기기가 실제로 많다 + (Mali 대부분, 구형 Adreno). 타일 연산은 행렬 유닛이 없으면 SIMT 코드로 낮춰진다. +- 커널 안: 고정 dtype 스칼라(f32, f16, bf16, int8 포함), 버퍼·텐서 뷰, 산술, 제어 흐름, 배리어. 객체 생성·힙 할당·예외·파이썬 호출은 없다. 위반은 등급 1 과 같은 게이트에서 컴파일 에러. -- 커널 함수도 유효한 파이썬이다. CPU 에서 참조 구현으로 실행해 수치를 대조할 수 있다. - -### 5.3 컴파일 경로 - - @kernel 서브셋 → 자체 소형 IR → SPIR-V ─┬─ Vulkan (Android, 데스크톱) - ├─ SPIRV-Cross → MSL (iOS, macOS) - └─ naga → WGSL (웹) - └──────────── PTX (데스크톱 CUDA, 별도) - -한 소스에서 모바일·데스크톱·웹 타깃이 나온다. +- **커널 함수도 유효한 파이썬이다.** 타일 연산은 CPU 참조 구현을 가진 일반 파이썬 함수이고, 커널은 + CPython 에서 그대로 실행되어 수치를 대조할 수 있다. TileLang · Triton · JAX Pallas 와 갈리는 지점이다 + (Pallas 는 CPU 에서 `interpret=True` 로만, JAX 위에서 돈다). +- **참조 구현은 numpy 에 의존하지 않는다 (결정 2026-10-04).** 타일 연산의 CPU 참조는 순수 파이썬(표준 + 라이브러리의 `array`, `memoryview`, `struct`)으로 쓴다. 참조의 목적은 정답이지 속도가 아니다. 커널을 + JAX · torch · numpy 없이 CPython 만으로 실행할 수 있다는 것이 Pallas 대비 차별점이고, 첫 조각의 + 시험으로 박는다 (§6 의 4). + +### 5.3 CPU SIMD: `@compiled` 의 벡터화 + +Mojo 의 절반은 CPU SIMD 다. 등급 1 `@compiled` 의 C 백엔드(§4.3)가 이 자리를 맡는다. + +- 첫 단계는 **C 벡터 확장**(`__attribute__((vector_size))`)으로 NEON · AVX 를 내는 것이다. +- 행렬곱 같은 핫 커널은 손으로 쓴 NEON 경로를 둔다. +- 벡터 폭과 dtype 을 표현하는 타입은 등급 2 의 타일·dtype 표기와 같은 어휘를 쓴다. + +이 절은 조사가 얕다(investigations 의 [I] 표기). 첫 GPU 조각 다음에 별도로 설계한다. + +### 5.4 IR: 우리 것, MLIR 에 사상되게 + +프론트엔드(유효한 파이썬 커널을 읽는 부분)와 IR 은 우리가 만든다. "CPython 에서 그대로 실행" 원칙을 +지키는 길이 이것뿐이다. 다만 IR 은 **MLIR 의 gpu · vector · linalg 구조에 그대로 사상되게** 설계한다. +나중에 MLIR 로 옮기거나 둘 사이를 오가는 비용을 낮추기 위해서다. + +- **MLIR 파이썬 휠은 선택적 보조 도구다.** SPIR-V · PTX 직렬화, 검증, 교차 대조에 쓴다. 공식 휠은 + LLVM 의 nightly(eudsl, alpha)이고 PyPI 에 없으며, 3.14t 휠은 있으나 **3.15t 휠은 아직 없다**(조사 + 시점 2026-10-04). 그래서 3.14t 별도 프로세스에서 돌리고, 날짜를 고정한 휠을 잠금 파일에 둔다. 빠지면 + SPIR-V 를 직접 내는 쪽으로 돌아간다. +- MLIR 의 타일링 · 벡터 · transform 패스를 쓸지는 **첫 조각이 동작한 뒤** 정한다. +- 채택하지 않는 것(참고 자료로만): TileLang/TVM (Vulkan 백엔드 없음, 커널이 일반 파이썬으로 실행되지 + 않음), CubeCL (진입점이 Rust 매크로, JIT 전용), IREE (모델 단위 입력과 자체 출력 형식, SPIR-V 코드 + 생성만 떼어 쓸 공식 경로 없음), xDSL (spirv 방언·SPIR-V/MSL/PTX 출력기 없음). +- IREE 기각은 **커널 컴파일러로서의** 평가다. **모델 통째 배포** 관점에서는 JAX 가 StableHLO 를 내고 + IREE 가 그것을 모바일로 AOT 컴파일하는 길이 경쟁자로 남는다 (§5.10). + +### 5.5 출력기: 대상별로 교체 가능 + + @kernel (유효한 파이썬) → 프론트엔드 → 우리 IR ─┬─ SPIR-V ──── Vulkan (Android, 데스크톱) + ├─ MSL 직접 ─ Metal (iOS, macOS) + ├─ WGSL ───── WebGPU (웹) + ├─ PTX ────── CUDA (데스크톱) + └─ C + 벡터 확장 ─ CPU (참조 · 폴백) + +- **Apple 은 MSL 을 직접 낸다.** SPIRV-Cross 를 거치면 행렬 유닛이 8x8 float simdgroup 까지만 가고, + Metal 4 텐서 연산(MSL 4 전용)에는 닿지 않는다. +- **MSL 과 WGSL 출력기는 어느 IR 을 택해도 우리가 쓴다.** MLIR 상류에 두 대상이 없다. +- SPIR-V 와 PTX 는 직접 내거나 MLIR 휠로 직렬화한다 (§5.4). +- **커널 번들은 IR 버전과 출력기 버전을 기록한다.** 런타임은 맞지 않는 번들을 거부한다. `jax.export` 가 + 호환 창을 명시하는 것과 같은 자리다. + +### 5.6 런타임과 착지점: torchnative + +**별도 런타임을 만들지 않는다.** 커널은 torchnative 의 장치 계층에서 실행된다. + +- **Vulkan:** torchnative `torch_c/src/vulkan.rs` 는 이미 SPIR-V 를 싣고(`include_bytes!`), 파이프라인을 + 만들고, push constant 로 shape 을 넘겨 실행한다. 첫 조각은 glslc 로 만든 셰이더 자리에 우리 + 컴파일러가 만든 SPIR-V 를 넣는 일이다. +- **Metal:** candle-metal 경로. 우리 MSL 커널을 거기에 붙이는 방식은 torchnative 와 정한다 (§8). +- **wgpu 는 웹 전용이다.** torchnative 가 기기에서 두 방식을 나란히 재 보았다: ash 는 크레이트 4 개 + 719 KB, wgpu 는 60 개 6.77 MB 이고 torchnative 는 ash 를 권고했다(`docs/devices/VULKAN.md` §5.3). + wgpu 를 네이티브 런타임으로 쓰면 행렬 유닛이 실험 기능 뒤에 갇힌다. +- **착지:** 컴파일된 커널은 `torch.library` 커스텀 op, 또는 torchnative 가 빌드 타임으로 옮긴 HF + `kernels` 번들 리졸버의 항목으로 등록된다. 그러면 `transformers` 코드가 수정 없이 그 커널을 쓴다. + `kernels` 규격에 `vulkan` 슬롯이 없는 구멍을 이 커널이 메운다. +- **수치 기준:** torchnative CPU 경로(bf16/fp16 에서 비트 일치하는 `flash.rs` 등)가 정답지다. 커널의 + 일반 파이썬 실행, torchnative CPU, GPU 출력의 세 쪽을 대조한다. +- **자동 융합은 뒤로 미룬다.** torchnative 의 그래프 캡처로 `torch.compile` 을 대신하는 길(캡처한 + 그래프의 융합 구간을 커널 컴파일러로 넘김)은 가치가 크지만, 그 캡처는 동적 shape · in-place 변경 · + 제어 흐름을 거부하고 LLM 의 KV 캐시 갱신이 in-place 라 정면으로 걸린다. 그전까지는 리졸버를 통한 + 레이어 교체로 착지한다 (§5.9). + +### 5.7 빌드 타임 오토튜닝 + +같은 커널도 타일 크기, 파이프라인 단계 수, 워크그룹 크기에 따라 몇 배씩 차이가 난다. Triton · +TileLang · MLC-LLM 은 이것을 자동 탐색한다. 모바일에서는 실행 중에 탐색할 수 없으므로(§5.1) 탐색을 +**빌드 타임으로 옮긴다.** + +- 커널마다 조정 가능한 매개변수(타일 크기, 단계 수, 워크그룹 크기, 행렬 유닛 사용 여부)를 선언한다. +- **GPU 계열별로 실기기에서 탐색한다:** Adreno 세대, Mali, Apple GPU 세대, 데스크톱. 실기기 팜이나 + CI 의 기기 러너가 필요하다. +- 결과는 **설정표로 번들에 실리고**, 런타임은 기기를 식별해 표에서 고른다. 표에 없는 기기는 보수적인 + 기본 설정과 SIMT 폴백을 쓴다. +- 탐색 결과는 커널 소스, 컴파일러 버전, 드라이버 계열을 키로 캐시한다. 소스가 바뀌지 않으면 다시 + 탐색하지 않는다. + +탐색 없이는 첫 커널이 손으로 튜닝한 코드보다 느릴 가능성이 크다. 그래서 오토튜닝은 단계 5(타일 +행렬곱)의 일부다. + +### 5.8 성능 기준선 + +이 설계는 성능 상한을 막는 장애물을 치운 것이지, 속도를 보장하지 않는다. 경쟁력은 측정으로만 +주장한다. + +- **비교 대상:** 같은 기기에서 llama.cpp(Vulkan, Metal 백엔드)와 Apple MLX 의 softmax, matmul, + attention. 모바일에서 실제 경쟁 상대는 Mojo 가 아니라 이들과 MLC-LLM, ExecuTorch 다. 데스크톱 CUDA + 에서는 Triton · TileLang 이 기준이다. +- **첫 조각부터** 측정 테스트로 박는다(AGENTS.md §14). 기록하는 값은 커널 시간과 기준 대비 비율이고, + 수치 오차는 §5.6 의 세 쪽 대조로 따로 본다. +- **목표(안):** 모바일에서 llama.cpp · MLX 의 손 튜닝 커널에 근접하는 것. 구체적인 비율은 첫 측정 뒤 + 메인테이너가 정한다. +- 측정은 AGENTS.md §8 을 따른다: 단독 실행, 부하 확인, 필터 없이. + +### 5.9 자동 융합 (torchnative #68) + +LLM 디코딩은 작은 연산이 많아 연산 사이의 디스패치와 메모리 왕복이 커널만큼 시간을 먹는다. +`torch.compile` 이득의 대부분이 융합이다. §5.6 이 미룬 그래프 캡처 기반 융합(동적 shape, in-place KV +갱신 문제)은 torchnative #68 에서 다룬다. 그전까지 모델 전체 수준의 속도 경쟁력은 제한된다. + +### 5.10 JAX 와의 관계: 어디서 이기고, 무엇을 torch 에 맡기는가 + +> **결정 (2026-10-04, 메인테이너):** 설계는 JAX 대비 장점이 분명해야 한다. **torch 가 JAX 만큼 +> 성능을 내는 부분만 torch 에 맡기고**, 그렇지 않은 부분은 목록으로 두어 따로 판단한다. 근거는 +> [`typedpython-vs-jax.md`](../investigations/typedpython-vs-jax.md). + +**포지셔닝.** "JAX 보다 낫다" 가 아니라 **"JAX 가 가지 않는 곳에서 이긴다"** 로 쓴다. 근거가 이 범위에서만 +선다. + +| 우리가 이기는 곳 | 근거 (조사 2026-10-04, 웹 요약 경유) | +|---|---| +| 모바일 GPU(Vulkan, Metal)가 1급 대상 | Pallas 의 GPU 하강은 Mosaic GPU(Hopper 이상)이고 Triton 백엔드는 deprecated, Vulkan/Metal 백엔드 없음. jax-metal 은 macOS 15.6 에서 실패하는 이슈가 열려 있다 | +| 파이썬 의미 보존 | `jit` 은 트레이싱이라 부작용이 트레이스 때만 실행되고, 값 의존 분기는 에러, 배열은 불변, 범위 밖 인덱스는 clamp 된다. Pallas 커널은 JAX 없이 실행되지 않는다 | +| 빌드 타임 타입 게이트 | jaxtyping 은 실행(트레이스) 시점 검사이고 정적 검사기에는 `Array` 로만 보인다. 단, 우리도 shape 오류는 정적으로 잡지 못한다 | +| 생태계 착지 | transformers v5 가 TF 와 Flax 를 제거하고 PyTorch 만 남겼다. torch op 착지(§5.6)가 그 생태계에 붙는다 | + +**torch 에 맡기는 조건.** JAX 의 강점 중 우리가 직접 만들지 않는 것은 torch 가 맡는다. 다만 **torch 가 +JAX 와 같은 수준의 성능을 낸다는 근거가 있을 때만** 맡긴다고 확정하고, 근거가 없으면 아래 목록에 둔다. +2026-10-04 현재 어느 항목도 측정 근거가 없으므로 **전부 "판단 대기"** 다. + +| 기능 | JAX | torch 대응 | 판단 | +|---|---|---|---| +| 전체 프로그램 융합 | XLA | 데스크톱 `torch.compile`(Inductor). 모바일은 불가, torchnative 그래프 캡처로 대체 예정(#68, §5.9) | 판단 대기. **모바일에서는 알려진 격차** | +| 자동 미분 (`grad`) | 함수 변환, `jit` 과 합성 | `torch.autograd`, `torch.func.grad` | 판단 대기 (학습 성능 비교 없음. 온디바이스 목표상 추론이 우선) | +| 벡터화 (`vmap`) | `jit` 안에서 융합까지 | `torch.func.vmap` | 판단 대기 (`vmap` + 컴파일 조합의 성능 비교 없음) | +| 변환의 합성 (`jit`·`grad`·`vmap`) | 1급 | `torch.func` + `torch.compile` | 판단 대기 | +| 샤딩, 다중 기기 | `shard_map`, pjit | DTensor, FSDP | 판단 대기 (온디바이스 목표 밖일 가능성) | +| TPU · Hopper/Blackwell 커널 성숙도 | Mosaic TPU/GPU | torch_xla, Triton | 판단 대기 (데스크톱 CUDA 는 §5.8 의 Triton·TileLang 기준) | +| 내보낸 산출물의 호환 보장 | `jax.export` 후방 6 개월, 전방 3 주 | `torch.export`, ExecuTorch | 판단 대기. 우리 커널 번들은 IR 버전과 출력기 버전을 기록한다(§5.5) | -### 5.4 착지점 — torchnative +각 항목은 측정(같은 작업을 JAX 와 torch 로 실행한 시간)이나 메인테이너 판단으로 "torch 에 맡김", "우리가 +만든다", "범위 밖" 중 하나로 닫는다 (§8 의 8). -컴파일된 커널을 **`torch.library` 커스텀 op 로 등록**하고 mps / vulkan 구현을 붙인다. 그러면 -`transformers` 코드가 수정 없이 그 커널을 쓴다. +**불분명한 것.** -torchnative 의 설계는 이미 이 자리를 비워두고 있다. 융합 어텐션 같은 커널 계층을 "prefill 문제의 -예정된 해법" 으로 지목했고, 커널 소스 후보로 손으로 쓴 Metal/NEON, CubeCL, AOT Triton 을 올려 -두었다. TypedPython 등급 2 는 **같은 자리를 파이썬으로 채우는 네 번째 후보**다. +- **JAX + IREE 모델 배포.** `jax.export` 의 StableHLO 를 IREE 가 AOT 컴파일해 모바일에 싣는 길은 이론상 있다. + IREE 는 §5.4 에서 *커널 컴파일러로서* 기각했지만(코드 생성만 떼어 쓸 공식 경로 없음), *모델 통째 배포* + 관점에서는 경쟁자다. 실제로 iOS/Android GPU 에서 도는 사례는 찾지 못했다. +- **성능.** 양쪽 모두 모바일 측정이 없다. "JAX 보다 빠르다" 고 주장할 근거는 지금 없다 (§5.8). +- **데스크톱 CUDA(PTX) 와 Mosaic GPU.** 비교 근거가 없다. -### 5.5 선례 — Taichi +### 5.11 선례, Taichi 파이썬 문법으로 Vulkan / Metal 커널을 AOT 컴파일해 모바일에 실었던 가장 가까운 선례다. 지금은 활동이 크게 줄었다. 기술보다는 **독자 런타임과 독자 생태계로 고립된 것**이 원인으로 보인다. @@ -268,18 +592,22 @@ TypedPython 은 그래서 자체 텐서 런타임을 만들지 않고 **torch op | 1b | 빌드 기본 단계로 연결 | PythonMultiplatform Gradle 플러그인이 사용자 파이썬 소스에 게이트를 항상 돌리고, 에러면 빌드 실패 | 1 | | 2 | typed IR → C 백엔드 (#41) | 프론트엔드·검증기·C 생성·런타임, 데스크톱에서 끝까지 | 없음 (pypackpack 슬롯은 3 의 의존) | | 3 | Kotlin 직접 호출 | 스텁 타입 기반 C ABI 호출 생성 | 스텁 배선 완료 | -| 4 | 커널 하나 | 융합 softmax 를 Vulkan · Metal 양쪽에서 upstream 과 수치 일치 | torchnative 착지 승인 | -| 5 | 커널 컴파일러 일반화 | SIMT 서브셋 전체, 이후 타일 | 4 | +| 4 | 첫 커널 조각 | f16 원소별 연산 또는 softmax 하나를 SPIR-V 로 컴파일해 torchnative `vulkan.rs` 로 실행, 기기에서 일반 파이썬 실행 · torchnative CPU 와 수치 일치, llama.cpp 대비 시간 기록 (§5.8), 커널이 JAX · torch · numpy 없이 CPython 만으로 실행됨 (§5.2) (추정 2~4 주) | torchnative 의 임의 SPIR-V 실행 진입점 | +| 4b | Metal 조각 | 같은 커널을 MSL 로 직접 내 iOS · macOS 에서 수치 일치 | 4, candle-metal 연결 방식 (§8) | +| 5 | 타일 행렬곱 | 행렬 유닛(cooperative matrix, simdgroup_matrix)을 쓰는 타일 matmul, 없는 기기는 SIMT 폴백, 빌드 타임 오토튜닝 (§5.7) (추정 2~4 개월, MLIR 패스 사용 시 1~3 개월) | 4, 4b | +| 6 | 착지 | 커널을 `torch.library` op · `kernels` 번들 항목으로 등록, `transformers` 무수정 사용 | 5 | +| 7 | CPU SIMD | `@compiled` C 백엔드의 벡터 확장 · NEON 경로 (§5.3) | 2 | -각 단계는 AGENTS.md §5·§14 의 TDD 와 측정 테스트 규정을 따른다. 특히 3 은 경계 비용 감소를, 4 는 -upstream 대비 수치 오차를 기준선으로 박아 둔다. +각 단계는 AGENTS.md §5·§14 의 TDD 와 측정 테스트 규정을 따른다. 특히 3 은 경계 비용 감소를, 4 와 5 는 +upstream 대비 수치 오차와 커널 시간을 기준선으로 박아 둔다. 4 와 5 의 기간은 조사의 추정이지 측정이 +아니다. ### 6.1 단계 1 의 현재 상태 (2026-10-03, #16) - **위치:** `python-multiplatform-ksp/src/main/python/typedpython/`, 테스트는 `python-multiplatform-ksp/src/test/python/`. 사용자 지시("ksp랑 통합")에 따라 KSP 모듈 안에 둔다. KSP 프로세서는 Kotlin 심볼만 보고 파이썬 파일 변경으로는 다시 돌지 않으므로, **실행**은 1b 에서 - Gradle 태스크가 맡는다 — KSP 모듈은 소스와 배포(jar 리소스)의 자리다. + Gradle 태스크가 맡는다, KSP 모듈은 소스와 배포(jar 리소스)의 자리다. - **구현된 규칙:** SPEC N-7. Pyrefly 의 `Any` 유출 kind 6개를 에러로 올리고, Pyrefly 의 per-expression 타입 리포트(`--report-pysa`)로 사용자 코드 안의 `Any` 식을 찾는다(`any-flow`). `cast()` 인자, 버려지는 식 문장의 값, 함수 시그니처 자체는 제외한다. @@ -288,12 +616,12 @@ upstream 대비 수치 오차를 기준선으로 박아 둔다. `; x` 를 붙인 사본을 만들어(줄 번호 불변) 한 번 더 돌리고, 그 위치의 타입으로 판정한다. 두 번 이상 대입되는 이름이나 컨테이너 디스플레이가 있는 파일에서만 돈다. `for` 대상과 컴프리헨션 변수는 아직 탐침하지 않는다. -- **측정:** 1800줄 합성 모듈에서 plain `pyrefly check` 160–235 ms, 게이트 1.3–1.9 s — 두 번 잰 비율이 - 모두 약 8배다. 탐침 패스를 별도 실행으로 붙이면 약 15배(232 ms → 3.6 s, load 41)였다 — 합성 모듈의 +- **측정:** 1800줄 합성 모듈에서 plain `pyrefly check` 160–235 ms, 게이트 1.3–1.9 s, 두 번 잰 비율이 + 모두 약 8배다. 탐침 패스를 별도 실행으로 붙이면 약 15배(232 ms → 3.6 s, load 41)였다, 합성 모듈의 `total += ...` 가 탐침 대상이라 두 번째 실행이 돌았기 때문이다. 지금은 탐침 대상이 있으면 **탐침 사본(과 같은 검사의 나머지 파일)에 대해 Pyrefly 를 한 번만** 돌리고, 에러·`any-flow`· 타입을 모두 그 한 번에서 읽는다(에러 열은 삽입 길이만큼 왼쪽으로 되돌린다). 같은 합성 모듈에서 - 825 ms → 6.2 s, 약 7.6배(load 30 — 유효한 측정이 아니다. load 4 초과). 단독 측정이 아니다(두 번째는 load average 18.6). 절대값보다 비율을 기준으로 본다. 차이의 + 825 ms → 6.2 s, 약 7.6배(load 30, 유효한 측정이 아니다. load 4 초과). 단독 측정이 아니다(두 번째는 load average 18.6). 절대값보다 비율을 기준으로 본다. 차이의 대부분은 타입 리포트가 typeshed 사본까지 디스크에 쓰는 비용으로 보인다(추정, 미분해). 1b 에서 빌드마다 돌리기 전에 줄일 대상이다. - **고정:** Pyrefly `==1.3.2`. 타입 리포트는 Pyrefly 가 버전을 매기는 포맷이지 공개 API 가 아니다. @@ -306,29 +634,47 @@ upstream 대비 수치 오차를 기준선으로 박아 둔다. 그 목록이 2단계 IR 의 요구사항이 된다. - **등급 2 컴파일러의 규모.** 가장 큰 작업이다. 커널 하나를 끝까지 하는 것(§6 의 4)을 첫 검증 단위로 삼아 범위를 제한한다. +- **행렬 유닛의 실제 지원 범위.** Adreno 는 신형 칩에서 확장 두 개가 필요하고 Mali 는 대부분 없다. + SIMT 폴백이 기본 경로가 되는 기기가 많을 수 있다. 기기별 측정 전에는 성능을 약속하지 않는다. +- **오토튜닝에 필요한 실기기.** GPU 계열별 탐색(§5.7)은 기기 팜이나 기기 러너 없이는 못 한다. + 확보 전에는 계열 하나(개발 기기)의 설정표로 시작한다. +- **속도 경쟁력은 아직 주장할 수 없다.** 설계는 상한만 열었다. §5.8 의 기준선 측정 전에는 성능을 + 약속하지 않고, 자동 융합(§5.9)이 없으면 모델 전체 속도는 제한된다. +- **MLIR 휠의 변동.** nightly · alpha 이고 3.15t 휠이 없다. 선택적 보조로만 두고 날짜 고정 휠을 쓴다. + 빠져도 SPIR-V 를 직접 내는 경로가 남아야 한다 (§5.4). - **기본 검사의 오탐.** 검사가 기본값이므로, 오탐 하나가 모든 사용자의 빌드를 막는다. 1b 를 켜기 전에 이 저장소의 실제 사용자 측 파이썬 코드(`sample/`, pythonx-compose 의 소비 예시)에 게이트를 돌려 에러 수와 그 원인을 기준선으로 박아 둔다. - **서드파티 스텁 부재.** 스텁이 없는 의존성을 많이 쓰면 `Any` 출처가 늘어 에러가 잦아진다. 이 경우를 에러로 둘지, 경계 한 곳의 명시적 캐스트로 해소하게 할지가 채택을 가른다. +- **메모리 기대치.** 타입을 확정한 데이터만 Rust 근처로 간다. `dict`·`str`·일반 클래스 위주의 코드와 인터프리터 + 바닥은 크게 줄지 않는다 (§4.3.5). - **파이썬 의미론과의 괴리.** 금지 목록이 실제 코드에서 얼마나 흔한지 측정해야 한다. 너무 흔하면 `@compiled` 가 채택되지 않는다. ## 8. 미결 사항 (사용자 결정) -1. ~~`compiled` 의 표기~~ — **결정됨 (2026-10-03, 메인테이너):** `compiled` 는 **내장 이름**이다. 패키지 +1. ~~`compiled` 의 표기~~, **결정됨 (2026-10-03, 메인테이너):** `compiled` 는 **내장 이름**이다. 패키지 아래에 두지 않고(`@typedpython.compiled` 아님) import 없이 `@compiled` 로 쓴다. 런타임은 임베디드 인터프리터 시작 시 `builtins.compiled` 를 설치하고(#42), 일반 CPython 에서는 `typedpython` wheel 의 `typedpython_builtins.pth` 가 `builtins` 에 없을 때만 항등 함수를 설치한다(런타임이 넣은 것을 덮어쓰지 - 않는다). 타입 검사기에는 `__builtins__.pyi` 로 알린다 — Pyrefly 1.3.2 는 검색 경로의 그 파일을 추가 + 않는다). 타입 검사기에는 `__builtins__.pyi` 로 알린다, Pyrefly 1.3.2 는 검색 경로의 그 파일을 추가 내장으로 읽는다(실측). 모듈 단위 표기는 `# typedpython: compiled` 주석을 유지한다: `# pyright: strict` 와 같은 도구 pragma 형태이고, 실행 시 이름 조회가 필요 없어 일반 CPython 에서 부작용이 없으며, 데코레이터 없이 모듈 전체를 지정할 수 있다. -2. ~~고정폭 정수의 오버플로 의미~~ — **결정됨:** `int` 는 i64 로 컴파일하고 오버플로 시 인터프리터 재실행으로 큰 정수로 승격(의미 보존). -3. ~~pypackpack Cython 슬롯 인터페이스 소유~~ — **결정됨:** pypackpack 이 정한다. TypedPython 은 생성한 C 소스, 빌드 플래그, C 를 낸 모듈 목록을 넘긴다. 확장이 원본 소스를 품으므로 별도 `.py` 폴백은 없다(Cython 폐기 후 갱신). -4. GPU 착지를 torchnative 커스텀 op 로 할 것인가 (다른 레포 변경). -5. ~~TypedPython 을 별도 레포로 분리할 것인가~~ — **결정됨 (2026-10-02):** PythonMultiplatform 의 +2. ~~고정폭 정수의 오버플로 의미~~, **결정됨:** `int` 는 i64 로 컴파일하고 오버플로 시 인터프리터 재실행으로 큰 정수로 승격(의미 보존). +3. ~~pypackpack Cython 슬롯 인터페이스 소유~~, **결정됨:** pypackpack 이 정한다. TypedPython 은 생성한 C 소스, 빌드 플래그, C 를 낸 모듈 목록을 넘긴다. 확장이 원본 소스를 품으므로 별도 `.py` 폴백은 없다(Cython 폐기 후 갱신). +4. ~~GPU 착지를 torchnative 커스텀 op 로 할 것인가~~, **결정됨 (2026-10-04, 메인테이너):** 커널은 + torchnative 장치 계층에서 실행되고 torch op · `kernels` 번들로 착지한다 (§5.6). torchnative 와 정할 + 것은 남는다: Metal 커널을 candle-metal 에 붙이는 방식, 등록 경로를 ATen 디스패처와 `kernels` + 리졸버 중 무엇으로 정식화할지, Vulkan f16 · 행렬 유닛을 장치 계층에 넣을지. +5. ~~TypedPython 을 별도 레포로 분리할 것인가~~, **결정됨 (2026-10-02):** PythonMultiplatform 의 기본 동작. 검사는 기본, 컴파일은 opt-in. -6. 기본 검사에서 빠져나가는 방법을 둘 것인가 — 예: 동적 기능이 꼭 필요한 모듈 하나를 `checked` 에서 +6. 기본 검사에서 빠져나가는 방법을 둘 것인가, 예: 동적 기능이 꼭 필요한 모듈 하나를 `checked` 에서 빼는 표기. 두지 않으면 보장이 강해지고, 두면 이식이 쉬워진다. 7. §3.1 금지 목록을 `checked` 에서 경고로 둘 것인가, 에러로 둘 것인가. +8. **§5.10 의 "판단 대기" 목록.** 융합, 자동 미분, `vmap`, 변환 합성, 샤딩, TPU · 최신 GPU 성숙도, 내보낸 + 산출물의 호환 보장 각각을 "torch 에 맡김" (torch 가 JAX 만큼 성능을 낸다는 근거가 있을 때만), + "우리가 만든다", "범위 밖" 중 하나로 정한다. +9. ~~메모리와 인터프리터 바닥 (D1~D8)~~, **결정됨 (2026-10-05, 메인테이너):** D1 예, D2 예(조건부), D3 예(조건부), + D4 아니오, D5 결함 수정(#193), D6 예(앱 배포 빌드만), D7 예, D8 지금은 아니오. 내용은 §4.3.5. diff --git a/docs/design/upcall.md b/docs/design/upcall.md index aaf9a4076..761fd17c6 100644 --- a/docs/design/upcall.md +++ b/docs/design/upcall.md @@ -1,4 +1,4 @@ -# Upcalls — Python → Kotlin +# Upcalls, Python → Kotlin How Python code imports Kotlin packages, constructs Kotlin classes, calls their functions (including `suspend` ones) and reads their properties. The opposite direction is @@ -60,8 +60,8 @@ The equivalent here: | Once per name | Python passes a name (`"demo.Counter.increment"`); **Kotlin resolves it** in the table and returns a `CallableHandle` | | Every call after that | The handle crosses, never the string | -Passing a string per call costs `PyUnicode` → UTF-8 (an allocation) + the boundary + hashing + comparison — -hundreds of ns — whatever the table looks like. `UpcallTable.resolve` returns a `CallableHandle` whose raw +Passing a string per call costs `PyUnicode` → UTF-8 (an allocation) + the boundary + hashing + comparison, +hundreds of ns, whatever the table looks like. `UpcallTable.resolve` returns a `CallableHandle` whose raw value packs the table **epoch** (high 32 bits) and the index (low 32), so a handle cached across a table reinstall is rejected rather than calling a different function (`python-multiplatform/src/commonMain/kotlin/python/multiplatform/reflection/ExposedCallable.kt`, @@ -105,15 +105,17 @@ Every `public` declaration is registered; `@PythonInternal` opts a class or memb (`@PythonAPI`) was rejected (AGENTS.md §12.7; [`binding-policy.md`](binding-policy.md)). This is the premise the tree-shaking decision (§2.6) rests on and must not be re-litigated there. -### 1.6 Kotlin names only +### 1.6 Kotlin namespaces, Kotlin names and their Pythonic aliases -A Kotlin package is importable under its own fully-qualified name and nothing else. Functions on a +A Kotlin package is importable under its own fully-qualified name and nothing else: no namespace is +renamed and no synthetic `pythonx` module exists (INTENT §2.2–2.3, AGENTS.md §12.1–12.2). Functions on a Kotlin-named module take keyword arguments by **Kotlin parameter names**, honour Kotlin defaults for omitted parameters, and expose their signature through `inspect.signature` and `python_multiplatform.describe` (SPEC U-8; contract in the KDoc of -`python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/KotlinSurface.kt`). Nothing is -renamed: no snake_case, no synthetic `pythonx` module (INTENT §2.2–2.3, AGENTS.md §12.1–12.2). Making the -surface Pythonic is pythonx-compose's job, built on this metadata. See §4.3. +`python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/KotlinSurface.kt`). Beside +every Kotlin name, the declaration, member or keyword is also reachable by its Pythonic (snake_case) +alias when that alias is unambiguous (SPEC U-12, issue #131). This was removed in `238119b7` on a +misreading of §12.1 and restored; §12.1 now says it governs namespaces only. See §4.3. --- @@ -122,7 +124,7 @@ surface Pythonic is pythonx-compose's job, built on this metadata. See §4.3. ### 2.1 Fragments, one per module The KSP processor (`python-multiplatform-ksp/`) is shipped as an artifact and runs in **every** module that -exposes Kotlin to Python — including the user's own modules, since exposing the user's classes is the +exposes Kotlin to Python, including the user's own modules, since exposing the user's classes is the point. Each module emits one **fragment object** into the well-known package `python.multiplatform.generated.fragments`: @@ -130,8 +132,8 @@ point. Each module emits one **fragment object** into the well-known package // GENERATED by python-multiplatform-ksp. Do not edit. package python.multiplatform.generated.fragments -object Fragment_io_github_thisisthepy_ksp_fixtures_library : python.multiplatform.reflection.FunctionTableFragment { - override val moduleName: String = "io_github_thisisthepy_ksp_fixtures_library" +object Fragment_io_github_thisisthepy_python_multiplatform_ksp_fixtures_library : python.multiplatform.reflection.FunctionTableFragment { + override val moduleName: String = "io_github_thisisthepy_python_multiplatform_ksp_fixtures_library" override fun entries(): List = listOf( ExposedCallable(name = "fixture.library.greet", arity = 1, paramTypes = listOf(TypeTag.STRING), returnType = TypeTag.STRING, paramNames = listOf("name"), callable = { args -> fixture.library.greet(args[0] as String) }), @@ -145,8 +147,8 @@ object Fragment_io_github_thisisthepy_ksp_fixtures_library : python.multiplatfor - **Name**: `Fragment_`, where `moduleName` is the KSP option `python.multiplatform.moduleName` with `.` and `-` replaced by `_` (`Constants.kt`). The Gradle plugin - derives it from the **Maven group plus the project path** (`:ksp-fixtures:app` with group - `io.github.thisisthepy` → `io_github_thisisthepy_ksp_fixtures_app`), because fragments from every + derives it from the **Maven group plus the project path** (`:python-multiplatform-ksp:fixtures:app` with group + `io.github.thisisthepy` → `io_github_thisisthepy_python_multiplatform_ksp_fixtures_app`), because fragments from every artifact share one package and two libraries both called `:core` would otherwise collide. The name is not load-bearing for discovery (the whole package is scanned) but prevents collisions. - **Visibility: `public`.** An `internal` fragment does not work: `internal` is enforced per Kotlin @@ -189,10 +191,10 @@ object Fragment_io_github_thisisthepy_ksp_fixtures_library : python.multiplatfor | `@PythonInternal` | no | opt-out | Asserted against KSP-generated fragments, not hand-written ones: -`ksp-fixtures/app/src/desktopTest/.../GeneratedDeclarationKindsTest.kt`, -`GeneratedSuspendTest.kt`, `GeneratedTableTest.kt` (which runs `UpcallTableTest`'s scenarios — constructor +`python-multiplatform-ksp/fixtures/app/src/desktopTest/.../GeneratedDeclarationKindsTest.kt`, +`GeneratedSuspendTest.kt`, `GeneratedTableTest.kt` (which runs `UpcallTableTest`'s scenarios, constructor / method / getter / setter round trip through `HandleTable`, `@PythonInternal` exclusion, narrow -`Int`/`Float` widening, `tp_traverse` field detection — against a generated table), and +`Int`/`Float` widening, `tp_traverse` field detection, against a generated table), and `python-multiplatform-ksp/src/test/.../SourceRenderingTest.kt`. ### 2.3 Aggregation in the app module @@ -223,8 +225,8 @@ different fragments is an error**, detected before anything is mutated (SPEC U-1 **Discovery** uses `resolver.getDeclarationsFromPackage("python.multiplatform.generated.fragments")`, in two KSP rounds: -1. **Round 1** — generate the app's own `Fragment_`. The new file triggers round 2. -2. **Round 2** — `getDeclarationsFromPackage` now sees the app's fresh fragment **and** every dependency's +1. **Round 1**: generate the app's own `Fragment_`. The new file triggers round 2. +2. **Round 2**: `getDeclarationsFromPackage` now sees the app's fresh fragment **and** every dependency's fragment, which is on the compilation classpath as compiled classes (JAR) or klib metadata. Generate `FunctionTable`. @@ -235,17 +237,17 @@ expensive because it scans the whole classpath; the package query is cheap. (A m `getDeclarationsFromPackage` is `@KspExperimental`. It is the documented cross-module API, nothing non-experimental does the job, and it has been stable since KSP 1.x. Discovery sits behind the -`FragmentDiscovery` interface (`FragmentDiscovery.kt`) so a fallback — each module writing a metadata file -under `META-INF/`, read by the aggregator — can replace it without changing the fragment or table shape. +`FragmentDiscovery` interface (`FragmentDiscovery.kt`) so a fallback, each module writing a metadata file +under `META-INF/`, read by the aggregator, can replace it without changing the fragment or table shape. The same mechanism serves every target: | Platform | Discovery source | Status | |---|---|---| -| JVM desktop | JARs on the classpath | `ksp-fixtures/app` desktop tests | -| Android | JAR/AAR classpath | `ksp-fixtures/android` (`GeneratedAndroidTableTest`, unit and instrumented) | +| JVM desktop | JARs on the classpath | `python-multiplatform-ksp/fixtures/app` desktop tests | +| Android | JAR/AAR classpath | `python-multiplatform-ksp/fixtures/android` (`GeneratedAndroidTableTest`, unit and instrumented) | | GraalVM native image | classpath at build time; the table is statically reachable, so no reflection config | verified manually, [`graal-native-image-verification.md`](../platforms/graal-native-image-verification.md) (SPEC U-6) | -| Kotlin/Native | klib metadata | `ksp-fixtures/{library,app}` on `androidNativeArm64`: round 2 found the library's fragment in its `.klib`, `FunctionTable` compiled and linked, and `ksp-fixtures/app/src/androidNativeArm64Test/.../NativeSmokeTest.kt` runs it (SPEC U-3) | +| Kotlin/Native | klib metadata | `python-multiplatform-ksp/fixtures/{library,app}` on `androidNativeArm64`: round 2 found the library's fragment in its `.klib`, `FunctionTable` compiled and linked, and `python-multiplatform-ksp/fixtures/app/src/androidNativeArm64Test/.../NativeSmokeTest.kt` runs it (SPEC U-3) | `ServiceLoader` could work on JVM/Android, but one aggregator everywhere avoids a platform split and runtime classpath scanning. @@ -307,10 +309,10 @@ derives `moduleName`. Notes from building it: through one reflective lookup of `arg(String, String)`. - **Test configurations are matched as a camel-case word `Test`.** AGP puts the build type last (`kspAndroidTestDebug`), so an `endsWith("Test")` check once put the processor on a unit-test - compilation, which emitted a duplicate fragment shadowing `main`'s. `ksp-fixtures/android` carries the + compilation, which emitted a duplicate fragment shadowing `main`'s. `python-multiplatform-ksp/fixtures/android` carries the Android plugin for this reason ([ROADMAP](../roadmap/ROADMAP.md) §13). -### 2.6 Tree shaking — decision: accept the cost (measured) +### 2.6 Tree shaking, decision: accept the cost (measured) If the aggregator references every fragment and every fragment references every `public` callable, the linker sees the entire public API as reachable. Each lambda `{ args -> greet(args[0] as String) }` is a @@ -319,7 +321,7 @@ static reference to `greet`; a reachable fragment makes every target reachable. cannot all hold: (1) blacklist exposure, (2) a full table referenced at build time (required on Kotlin/Native), (3) tree shaking of unused entries. -**Chosen: give up (3) — accept the cost**, because (1) is settled (§1.5) and (2) is forced by §2.3. The +**Chosen: give up (3), accept the cost**, because (1) is settled (§1.5) and (2) is forced by §2.3. The remaining options, one line each: | Option | Verdict | @@ -329,8 +331,8 @@ remaining options, one line each: | D. `ServiceLoader` on JVM, aggregator on Native | A platform split for no gain: the JVM does not tree-shake anyway | | E. Two-tier table (skeleton + lazily initialised body) | Saves runtime lambda objects, **not binary size**: the `when` arms still statically reference every target on Kotlin/Native | -**Measured.** `ksp-fixtures/library` got 200 synthetic top-level functions (`fun bulkN(x: Long): Long = x + N`); -`ksp-fixtures/app`'s `androidNativeArm64` debug test binary was linked before and after +**Measured.** `python-multiplatform-ksp/fixtures/library` got 200 synthetic top-level functions (`fun bulkN(x: Long): Long = x + N`); +`python-multiplatform-ksp/fixtures/app`'s `androidNativeArm64` debug test binary was linked before and after (`linkDebugTestAndroidNativeArm64`) and stripped with the NDK's `aarch64-linux-android-strip`: | | bytes | delta | @@ -342,27 +344,27 @@ remaining options, one line each: This is a **floor**: each function is a one-line expression, so almost all of the 1.84 KB is the `ExposedCallable`, its lambda, its name string and Kotlin/Native per-function metadata. A real library adds -its function bodies on top. For 200 entries the floor is ≈11% of a 3.26 MB stripped baseline — noticeable, +its function bodies on top. For 200 entries the floor is ≈11% of a 3.26 MB stripped baseline, noticeable, not disqualifying. Whether that is acceptable for a real library is a product judgement; the only real mitigation would be changing the exposure model, which is settled. -### 2.7 Incremental KSP — measured, `ALL_FILES` stays +### 2.7 Incremental KSP, measured, `ALL_FILES` stays `Dependencies(false)` would never regenerate the aggregator and miss a newly added fragment; `Dependencies.ALL_FILES` is correct but reprocesses on every change. A narrower declaration buys nothing. -Measured with `ksp.incremental.log=true` on `:ksp-fixtures:app` (two files), modifying one: +Measured with `ksp.incremental.log=true` on `:python-multiplatform-ksp:fixtures:app` (two files), modifying one: | aggregator `Dependencies` | dirty / all | |---|---| | `ALL_FILES` | 100.00% | | `Dependencies(aggregating = true, )` | 100.00% | -A module's own `Fragment_` is an *aggregating* output over every source file — correctly, since under -blacklist exposure any file can add an entry — so any change regenerates it and KSP marks every source that -maps to it dirty. A classpath-only change behaves the same (adding a function to `:ksp-fixtures:library` +A module's own `Fragment_` is an *aggregating* output over every source file, correctly, since under +blacklist exposure any file can add an entry, so any change regenerates it and KSP marks every source that +maps to it dirty. A classpath-only change behaves the same (adding a function to `:python-multiplatform-ksp:fixtures:library` dirtied both app files with `CP changes` listing `Fragment_ksp_fixture_library`). The only route to real incrementality is **per-file fragments**, which changes fragment naming, `UpcallTable`'s per-module -idempotency and where duplicate names are detected — a design change, not done. +idempotency and where duplicate names are detected, a design change, not done. ### 2.8 Alternatives rejected @@ -410,7 +412,7 @@ Argument passing needs exactly one C shape: PyObject *pm_invoke(long callableHandle, PyObject *args) // (long, long) -> long Arity and types travel inside the tuple and the table entry, never in the C signature. A stub specialised -per signature would need one per `(arity, tag-vector)` — unbounded and impossible to pre-generate for a +per signature would need one per `(arity, tag-vector)`, unbounded and impossible to pre-generate for a closed world. The remaining slots were already in the downcall vocabulary: | Slot | C signature | Carrier shape | | @@ -423,7 +425,7 @@ closed world. The remaining slots were already in the downcall vocabulary: | name → handle | `long(const char *)` | `(long) -> long` | pre-existing | | release / cancel | `int(long)` | `(long) -> int` | pre-existing (`_pm_release`, `_pm_cancel`) | -`newfunc` is unaccounted for only if `tp_new` is ever bound directly; it is not — construction goes +`newfunc` is unaccounted for only if `tp_new` is ever bound directly; it is not, construction goes through a `CONSTRUCTOR` entry called from Python (§4.2). ### 3.3 What crosses, per tag @@ -435,25 +437,25 @@ The `Array` an entry receives has one representation per `TypeTag`, which |---|---|---| | `INT` | `int` | `Long` (a declared `Int`/`Short`/`Byte` is narrowed by generated code) | | `FLOAT` | `float` | `Double` | -| `BOOLEAN` | `bool` | `Boolean` — a distinct Python type, not folded into `INT` | +| `BOOLEAN` | `bool` | `Boolean`, a distinct Python type, not folded into `INT` | | `STRING` | `str` | `String` | | `BYTES` | `bytes` | `ByteArray` (NUL bytes survive; SPEC U-2) | | `UNIT` | `None` | `Unit` | | `OBJECT` | `int` handle, **or** any Python object | the Kotlin instance from `HandleTable`, **or** a `PyObject` | -| — | `None` | `null`, whatever the tag | +| - | `None` | `null`, whatever the tag | `OBJECT` is resolved by what Python actually sent: Python cannot hold a Kotlin reference, so a Kotlin object crosses as an `ObjectReference` integer (`(generation shl 32) | slot`, generation ≥ 1); anything else is a Python object and reaches a parameter declared `PyObject`. -`BYTES` is correct and slow — one item at a time — because `PyBytes_AsString` is bound as a NUL-terminated +`BYTES` is correct and slow, one item at a time, because `PyBytes_AsString` is bound as a NUL-terminated string read and `PyBytes_AsStringAndSize` is in no platform's `EmbedAPI` yet (`UpcallTrampoline.toByteArray` KDoc). When it is, both directions collapse to one call. ### 3.4 Conventions that are not negotiable - **Arguments are borrowed.** `PyTuple_GetItem` lends, so a `PyObject` built over one takes - `borrowed = true`. `borrowed = false` gives back a reference nobody took — one per call — and the free + `borrowed = true`. `borrowed = false` gives back a reference nobody took, one per call, and the free lands somewhere unrelated. That bug crashed this repository twice. - **The result is a new reference**; Python takes ownership. Measured end to end through each platform's real entry point: a hundred calls move the returned object's refcount by zero @@ -479,8 +481,8 @@ runtimes the moment upcalls exist ([`androidMain/README.md`](../../python-multip ### 3.6 Handles and their lifetimes -- `CallableHandle` — table index + epoch (§1.2). `-1` is `NONE`; a valid handle is never negative. -- `HandleTable` / `ObjectReference` — a Kotlin object given to Python is rooted in `HandleTable` under a +- `CallableHandle`, table index + epoch (§1.2). `-1` is `NONE`; a valid handle is never negative. +- `HandleTable` / `ObjectReference`, a Kotlin object given to Python is rooted in `HandleTable` under a generation-tagged handle; releases are generational, so a second release is a no-op and a stale handle never frees a reused slot (SPEC M-4, `HandleTableTest`, `ProxyHandleLifetimeTest`, `OwnedResultLifetimeTest`). Generated proxies give their handle back in `__del__` (§4.2). @@ -513,7 +515,7 @@ came out of it: the generator's assumptions about bootstraps are only proven by ### 3.8 Android: the boundary runs the other way, and threads attach once -`RegisterNatives` binds a JVM `external fun` to a C function — the **downcall** direction. A +`RegisterNatives` binds a JVM `external fun` to a C function, the **downcall** direction. A `PyMethodDef`'s `ml_meth` must be a real C function pointer and Kotlin/JVM on ART can produce none. So the entry points are C functions in `jni_onload.def`, and *they* call Kotlin with `CallStaticLongMethod` against `UpcallCallbacks`, a class looked up once in `JNI_OnLoad` and held as a global ref. `RegisterNatives` appears @@ -526,8 +528,8 @@ for `tp_traverse`/`tp_clear` (`ProxyCallbacks`), down to reusing `pmp_attach`. `threading.Thread` and asserts it did not land on the instrumentation thread. **Attach once per thread, detach at thread death.** Attaching and detaching per call was the most expensive -thing on the Android upcall path (§7.6). The belief that forced it — "ART aborts if a thread exits without -detaching" — is false: `Thread::ThreadExitCallback` (`runtime/thread.cc`, same shape in `android-8.0.0_r1` +thing on the Android upcall path (§7.6). The belief that forced it, "ART aborts if a thread exits without +detaching", is false: `Thread::ThreadExitCallback` (`runtime/thread.cc`, same shape in `android-8.0.0_r1` and `main`) warns on its first invocation and only reaches `LOG(FATAL)` on a second, which only an `#else` branch Android does not compile could arm; bionic clears a key's value before its destructor and never re-reads it. `UpcallThreadAttachTest.aThreadThatExitsWithoutDetachingLeaksItsPeerRatherThanAbortingArt` @@ -537,7 +539,7 @@ lets an attached pthread exit undetached on `pmp_api26` and `pmp_api36` with no `pthread_key_create` destructor (`pmp_thread_exit_detach`) that defers to its second invocation so it cannot run before ART's own exit callback. -**Holding attachments is not a new risk.** Peak simultaneous attachments do not change — under the per-call +**Holding attachments is not a new risk.** Peak simultaneous attachments do not change, under the per-call scheme every worker upcalling at an instant is attached at that instant; the ceiling is the number of live Python threads either way. What changes is that release must be reliable: `manyConcurrentWorkersAreEachAttachedOnceAndAllReleased` holds 32 workers on a `threading.Barrier`, checks @@ -546,7 +548,7 @@ emulators). The instrument reads GC reachability of the peer, not `ThreadGroup.e does not report an attached native thread's peer at all; a positive control (peer unreclaimable while its worker is parked mid-upcall) is kept in the test because it is what caught that. -The C shims take no GIL — CPython holds it when calling a `PyCFunction` — and the trampoline takes its own +The C shims take no GIL, CPython holds it when calling a `PyCFunction`, and the trampoline takes its own pair (§3.5). ### 3.9 iOS and androidNative: the `@CName` + `ctypes.CDLL(None)` route, measured @@ -557,8 +559,8 @@ binary. On iOS **neither half holds**: | Binary | `pm_upcall_invoke` / `pm_upcall_resolve` / `pm_upcall_release_object` | |---|---| | androidNative `libmultiplatform_python3.14.so` | **exported** (`T` in `nm -D`) | -| iOS `PythonMultiplatform.framework` | **absent** — a K/N framework exports the Objective-C surface plus the Konan runtime | -| Kotlin/Native test executable, either target | **absent**, also from per-file caches — never emitted | +| iOS `PythonMultiplatform.framework` | **absent**, a K/N framework exports the Objective-C surface plus the Konan runtime | +| Kotlin/Native test executable, either target | **absent**, also from per-file caches, never emitted | and this project's iOS `Python.framework` binary has no `_ctypes`, so `import ctypes` raises `ModuleNotFoundError` (Android's CPython ships `_ctypes.so`). The `@CName` functions @@ -577,7 +579,7 @@ puts the export in CPython's table (`pmpRegisterUpcall`) and builds `PyCFunction objects over it. A `PyCFunction` carries `self` as well as the function pointer, so one export already backs any number of -distinct callables. Putting a value no handle can take in `self` turns it into a dispatcher — **no second +distinct callables. Putting a value no handle can take in `self` turns it into a dispatcher, **no second export, and no op argument**: | `self` | Name | Shape | @@ -598,7 +600,7 @@ guard ("the raw upcall entry points are not bound …"), so the passing run depe The bare mechanism (`call_indirect`, no arguments) was measured at 3.1 ns in the former standalone `wasm-experiment` (against 10.9 ns for `addFunction` around a JS closure); that experiment is now `wasmJsTest/.../WasmUpcallRouteOverheadTest.kt`, measured from Python as in production. **3.1 ns must not be -quoted for an upcall that carries arguments** — that is ~300 ns (§7.2). +quoted for an upcall that carries arguments**, that is ~300 ns (§7.2). --- @@ -622,34 +624,34 @@ installed table; `install()` executes it. Not a KSP-emitted `.py` resource, beca ### 4.2 Shapes -- **Module functions** — published into `sys.modules` under the Kotlin package (`_pm_module`), so +- **Module functions**: published into `sys.modules` under the Kotlin package (`_pm_module`), so `from demo.calc import doubleLater` works with no import hook (CPython checks `sys.modules` before any finder). Each is wrapped by `python_multiplatform.kotlin_function` (§4.3). -- **Classes** — a plain Python `class` whose `__init__` calls the `CONSTRUCTOR` entry and stores the handle in +- **Classes**: a plain Python `class` whose `__init__` calls the `CONSTRUCTOR` entry and stores the handle in `self._pm_handle`; methods and properties pass it as `args[0]`. A `private set` property refuses assignment. Interfaces and abstract classes have no constructor. -- **Handle release** — `__del__` (`tp_finalize`, run by `tp_dealloc`) calls `_pm_release`, looked up once at +- **Handle release**: `__del__` (`tp_finalize`, run by `tp_dealloc`) calls `_pm_release`, looked up once at class-definition time and carried as a default argument (globals are `None` during finalisation). Without it, `GeneratedProxyCostTest`'s constructor row ended at 78 002 live handles. Alternatives measured on CPython 3.13, construct + destruct net of a plain object: `__del__` +66 ns; per-instance holder +158 ns; `weakref.ref` registry +260 ns; `weakref.finalize` +555 ns. The accepted weakness: a subclass that defines `__del__` without calling `super().__del__()` leaks its handle (measured). `ProxyHandleLifetimeTest` pins release, including through a cycle. -- **GC base** — where `ProxyTypeFactory.installGcBase()` succeeds it publishes the cycle-collecting +- **GC base**: where `ProxyTypeFactory.installGcBase()` succeeds it publishes the cycle-collecting `PyType_FromSpec` type as `_pm_proxy_base`, and generated classes subclass it through a `PyMemberDef` named `_pm_handle`, so `self._pm_handle = …` writes into the C slot `tp_traverse` reads (§6). Otherwise they fall back to `_PmObject`. Every platform's `actual` now implements `installGcBase` (code read; the `expect`'s KDoc saying Android and wasm return `false` is stale). -- **Class statics** (companion / `object` properties, enum entries) — a descriptor on a **metaclass** rendered +- **Class statics** (companion / `object` properties, enum entries), a descriptor on a **metaclass** rendered beside the class, so instances cannot see them (Kotlin's rule). **Companion functions** go on the metaclass too: a `staticmethod` in the class body would be reachable through an instance, and an accessor pair would disagree with the Kotlin declaration. -- **Top-level `val`/`var`** — a `property` (a *data* descriptor) on a `ModuleType` subclass generated **per +- **Top-level `val`/`var`**: a `property` (a *data* descriptor) on a `ModuleType` subclass generated **per module** (one shared type would answer the attribute on every module). The previous shape, a `__getattr__`/`__setattr__` pair on one shared subclass, cost ~50× more per read because `__getattr__` is the fallback hook: `module_getattro` first raises a fully formatted `AttributeError` that the hook discards (§7.7). -- **`suspend` functions** — `async def` wrappers that await the result only if it is awaitable: +- **`suspend` functions**: `async def` wrappers that await the result only if it is awaitable: async def _pm_f_0(a0): _pm_r = _pm_invoke(_pm_h_0, (a0,)) @@ -672,9 +674,10 @@ lives at `python_multiplatform.binding`). Its KDoc is the contract (SPEC U-8): - `inspect.signature(fn)` (built lazily via `__wrapped__`) and `python_multiplatform.describe(fn)` give the metadata. The answer is the same whichever installer ran first (stamped epochs). -No member or parameter is renamed and the binder creates no `pythonx` module; a real `pythonx` package on -disk is what `import pythonx` loads. Tests: `desktopTest/.../pythonx/KotlinNamedSurfaceTest.kt`, -`ksp-fixtures/compose/.../KotlinSignatureMetadataTest.kt`. `.pyi` stubs are generated by the Gradle plugin +No namespace is renamed and the binder creates no `pythonx` module; a real `pythonx` package on +disk is what `import pythonx` loads. Members and keywords also answer to their Pythonic aliases, and the +signature shows the Pythonic keyword (SPEC U-12). Tests: `desktopTest/.../pythonx/KotlinNamedSurfaceTest.kt`, +`python-multiplatform-compose/fixtures/compose/.../KotlinSignatureMetadataTest.kt`. `.pyi` stubs are generated by the Gradle plugin under Kotlin module paths only (SPEC B-7). ### 4.4 Inheritance @@ -714,24 +717,24 @@ demonstrates it (§5.8); `planned` on wasm. ### 5.1 Why a suspend function cannot simply be called CPython calls a C function pointer whose frame must return a `PyObject *` synchronously; a suspension has -nothing to return. `runBlocking` is not a solution: it blocks the thread the upcall arrived on — on Android a -CPython-created pthread — so the interpreter waiting for the answer is the thing being blocked. The only +nothing to return. `runBlocking` is not a solution: it blocks the thread the upcall arrived on, on Android a +CPython-created pthread, so the interpreter waiting for the answer is the thing being blocked. The only option: **return something else now and deliver the value later.** ### 5.2 Three candidates; (C) chosen, with (B) as its mechanism -All three share the Kotlin half — start the coroutine inside the synchronous frame and park the result where +All three share the Kotlin half, start the coroutine inside the synchronous frame and park the result where it can be addressed later. | Candidate | Verdict | |---|---| | **(A) handle + polling** (`is_done(h)` / `result(h)`) | **Rejected.** On wasmJs Python and Kotlin share one JS thread and resumption only happens when control returns to the host, so a polling loop that never yields prevents its own progress (inferred). Also burns a core and re-takes the GIL at every switch interval, and KSP cannot expose a generic `Deferred` | -| **(B) callback** — Python passes a callable, Kotlin calls it on completion | Works: a Kotlin-created thread can take the GIL and call Python while the main thread is inside `run_until_complete` (measured, `AsyncCompletionProbeTest.aKotlinCreatedThreadCanResolveAnAsyncioFutureTheInterpreterIsWaitingOn`, which passes only after observing `loop.is_running()`). Hands all synchronisation to the user | -| **(C) asyncio** — Kotlin creates a `Future` on the running loop and resolves it with `loop.call_soon_threadsafe` | **Chosen.** Needs no new binding: `PyImport_ImportModule`, `PyObject_GetAttrString` and `PyObject_Call*` are enough (measured, `AsyncCompletionProbeTest.everyApiThisNeedsIsAlreadyBoundSoAnAsyncioConventionAddsNoNewBinding`). `await kotlin_fn(x)` is syntax Python users already know | +| **(B) callback**, Python passes a callable, Kotlin calls it on completion | Works: a Kotlin-created thread can take the GIL and call Python while the main thread is inside `run_until_complete` (measured, `AsyncCompletionProbeTest.aKotlinCreatedThreadCanResolveAnAsyncioFutureTheInterpreterIsWaitingOn`, which passes only after observing `loop.is_running()`). Hands all synchronisation to the user | +| **(C) asyncio**, Kotlin creates a `Future` on the running loop and resolves it with `loop.call_soon_threadsafe` | **Chosen.** Needs no new binding: `PyImport_ImportModule`, `PyObject_GetAttrString` and `PyObject_Call*` are enough (measured, `AsyncCompletionProbeTest.everyApiThisNeedsIsAlreadyBoundSoAnAsyncioConventionAddsNoNewBinding`). `await kotlin_fn(x)` is syntax Python users already know | (C) is mechanically (B): `call_soon_threadsafe` is a downcall from the completion thread. `Py_AddPendingCall` (in the Stable ABI, not bound in `EmbedAPI`) was not needed. The cost of (C): the application must be written -async — the loop must be running. +async, the loop must be running. **The fast path comes before the convention.** `suspend` is a signature, not a promise to suspend: a body that reaches no suspension point is complete before `PendingCall.start` returns @@ -741,7 +744,7 @@ directly with no `Future` and no event loop. What would reverse this choice: if a target's loop could not be made to yield to its host, (B) would be the better public surface there; if a completion thread on Android were found to need an ART attach, the `pmp_thread_exit_detach` destructor (§3.8) would have to be replicated on the completion side. (A Kotlin/JVM -thread is already known to ART, so no attach is expected — inferred; the ART sample in §5.8 resumes from a +thread is already known to ART, so no attach is expected, inferred; the ART sample in §5.8 resumes from a `java.lang.Thread` and works.) ### 5.3 KSP: a different body, not an exclusion @@ -755,8 +758,8 @@ The braces are a `suspend` lambda; it compiles with the stdlib only. `kind` is u member is still `METHOD`, receiver in `args[0]`) and `returnType` is the **declared** return type, so the fast path marshals exactly like a synchronous entry. `ExposedCallable.isSuspend` is a flag rather than `SUSPEND_*` variants of `CallableKind` because the axes are orthogonal: `kind` says where the arguments are, -`isSuspend` what comes back. Pinned by `ksp-fixtures/app/.../GeneratedSuspendTest.kt` against -`ksp-fixtures/library/.../Suspending.kt`, which pairs every suspending declaration (top-level, member, +`isSuspend` what comes back. Pinned by `python-multiplatform-ksp/fixtures/app/.../GeneratedSuspendTest.kt` against +`python-multiplatform-ksp/fixtures/library/.../Suspending.kt`, which pairs every suspending declaration (top-level, member, companion, interface, object) with a non-suspending control in the same scope. A `suspend` **function type** (`val h: suspend (Long) -> Long`, a parameter or return of that type, nullable, @@ -772,11 +775,11 @@ that; it is stale.) - **No `kotlinx.coroutines` dependency.** `startCoroutine` and `Continuation` are stdlib; the dispatcher a user's `suspend fun` needs is already on the user's classpath. - **The context is the `PendingCall` itself** (a `CoroutineContext.Element`), with no dispatcher, so the body - runs on the calling thread until its first real suspension — which is where the fast path comes from, and + runs on the calling thread until its first real suspension, which is where the fast path comes from, and why work before the first suspension runs **inside the C frame, holding the GIL**. - **Nothing escapes**: body and listener failures are parked, as in the trampoline. - **No new handle machinery**: it is an ordinary object in `HandleTable`. -- **Unsynchronised**, on the boundary's rule that every mutation happens on a GIL-holding thread; a +- **Unsynchronised**: on the boundary's rule that every mutation happens on a GIL-holding thread; a completion that reaches Python takes the GIL anyway. ### 5.5 Delivery @@ -795,10 +798,10 @@ really ensures; a completion inside a later upcall correctly nests. The Kotlin w reference to the `Future`; the completion lambda keeps the wrapper alive until it fires. **The fast path is wider than "did not suspend".** If the completion thread resumes the continuation while the -upcall frame is still running — `CFUNCTYPE` releases the GIL for the upcall itself — `deliver` sees `isDone` +upcall frame is still running, `CFUNCTYPE` releases the GIL for the upcall itself, `deliver` sees `isDone` and returns the value. Correct, but the synchronous-completion ratio is a function of the race, and a test of the slow path must observe that a `Future` actually crossed (`AsyncUpcallDeliveryTest` records -`type(r).__name__`; the proxy tests count `create_future` calls instead and assert `0` on the fast path — +`type(r).__name__`; the proxy tests count `create_future` calls instead and assert `0` on the fast path, `PythonProxyInstallTest.theSameGeneratedProxyBuildsNoFutureWhenTheKotlinBodyNeverSuspends`). **No running loop:** a call that really suspends fails with `RuntimeError: no running event loop`, and the @@ -812,7 +815,7 @@ A suspending entry that never suspends works with no loop at all **A completion landing on a cancelled `Future` is dropped, not raised.** Measured before the fix (`AsyncUpcallCancellationTest`): `await` got `CancelledError`; the loop's `call_exception_handler` got one `InvalidStateError: invalid state`; the completion thread's error indicator was clean and the next unrelated -upcall worked — no contamination, just an unactionable log entry. Two guards, only the second a guarantee: +upcall worked, no contamination, just an unactionable log entry. Two guards, only the second a guarantee: `AsyncUpcall.resolve` checks `done()` before scheduling (cheap, racy), and the scheduled callback is `_pm_settle`, which checks `done()` **again on the loop thread**. Removing only `_pm_settle`'s guard turns exactly the race test red; the race is forced, not left to timing (the Python coroutine spins without @@ -823,7 +826,7 @@ the coroutine's *completion*; the suspension point's continuation belongs to who `suspendCoroutine`, their dispatcher, `kotlinx.coroutines`). `PendingCall` never sees it, the stdlib cannot reach it, and resuming twice is undefined. Forced cancellation is what a `Job` tree does. What works: `ensureActive()` (top-level, reads `coroutineContext[PendingCall]`) throws `CancellationException` once the -call is cancelled — the same contract as `kotlinx.coroutines`, where a body that never checks is never +call is cancelled, the same contract as `kotlinx.coroutines`, where a body that never checks is never cancelled either. The element is not a `ContinuationInterceptor`, so the fast path is untouched. suspend fun slowSum(n: Long): Long { @@ -852,7 +855,7 @@ at completion), then resumes once and requires `CancellationException` and `isDo | Releaser | Only it covers | |---|---| -| Python done callback | a cancelled body that never cooperates — the Kotlin completion never runs | +| Python done callback | a cancelled body that never cooperates, the Kotlin completion never runs | | Kotlin `resolve` | the loop stopping before the scheduled done callback ran | Generational release makes the second a no-op and a late `_pm_cancel(stale)` harmless. Measured with @@ -864,11 +867,11 @@ Generational release makes the second a no-op and a late `_pm_cancel(stale)` har | after cancel, coroutine not yet finished | baseline (Python released it) | | after cancel and body exit | baseline | | after normal completion | baseline | -| fast path | baseline — nothing registered (`theFastPathRegistersNoHandleAtAll`) | +| fast path | baseline, nothing registered (`theFastPathRegistersNoHandleAtAll`) | Registration happens after `deliver`'s `isDone` early return, so the fast path touches neither `HandleTable`, `__main__` nor `add_done_callback`. The slow path pays two `__main__` lookups and one `_pm_watch` call. Not -reclaimed: a call whose `Future` never settles and whose coroutine never ends — that has leaked its +reclaimed: a call whose `Future` never settles and whose coroutine never ends, that has leaked its continuation already. ### 5.7 Desktop and Kotlin/Native tests @@ -876,7 +879,7 @@ continuation already. The desktop tests use `java.lang.Thread` and `java.util.concurrent`, so they cannot move to `commonTest`; the claims are re-verified on Kotlin/Native with `nativeTest/.../NativeThread.kt`, a thin `pthread_create` wrapper. Not `Worker`: the question is whether a completion may come from a thread the Kotlin/Native runtime -never attached itself, which `Worker` would sidestep — the same situation +never attached itself, which `Worker` would sidestep, the same situation `CycleCollectionTest.testDeallocOnAThreadCPythonCreated` measures for `tp_dealloc`. | `nativeTest` | Desktop counterpart | Re-verifies | @@ -887,33 +890,33 @@ never attached itself, which `Worker` would sidestep — the same situation All passed on the iOS simulator, three repeated runs. They are in `nativeTest`, which `androidNativeArm64Test` also runs, and they predate the androidNative full-suite runs recorded in §5.8 -(316 tests, 0 failures) — but no per-test androidNative result for them was recorded (unverified). -`AsyncUpcallPortabilityTest` (`commonTest`) keeps only the two paths that return before touching asyncio — fast -path and failure-before-suspend — which need no thread and run on all targets. +(316 tests, 0 failures), but no per-test androidNative result for them was recorded (unverified). +`AsyncUpcallPortabilityTest` (`commonTest`) keeps only the two paths that return before touching asyncio, fast +path and failure-before-suspend, which need no thread and run on all targets. ### 5.8 Per platform -**Desktop** — everything above (`AsyncUpcallDeliveryTest`, `AsyncUpcallCancellationTest`, +**Desktop**, everything above (`AsyncUpcallDeliveryTest`, `AsyncUpcallCancellationTest`, `AsyncUpcallEarlyCancellationTest`, `PythonProxyDeliveryTest`). -**iOS simulator** — asyncio is available. The iOS `Python.framework` binary has no `lib-dynload`, but asyncio +**iOS simulator**, asyncio is available. The iOS `Python.framework` binary has no `lib-dynload`, but asyncio comes from the separately extracted BeeWare stdlib (`build/python-stdlib/ios-simulator/lib/python3.14/` with `asyncio/` and `lib-dynload/_asyncio…so`, `select…so`, `_socket…so`). `iosSimulatorArm64Test/.../AsyncioAvailabilityProbeTest.kt` (isolated in its own file) runs `asyncio.new_event_loop().run_until_complete(...)` and passes. Delivery, early cancellation and the silent drop pass (§5.7). -**Android (ART)** — proxies, including `async` ones, run on `pmp_api26` and `pmp_api36` +**Android (ART)**, proxies, including `async` ones, run on `pmp_api26` and `pmp_api36` (`connectedDebugAndroidTest`: 336 tests, 0 failures on each; `PythonProxyInstallTest`'s 11 on the real path). The sample app on both emulators showed: | Section | Output | |---|---| -| 3 — upcall | `handle 4294967327 -> 0 · with args -> presses x3 = 0` (called from Python via `_pm_resolve`/`_pm_invoke`) | -| 5 — class proxy | `installed over PyMethodDef via JNI: 466 lines, 2 proxy classes`, `Greeter('Kotlin').greet(2) -> hello Kotlin! hello Kotlin!`, `g.greetings = 99 -> AttributeError (private set held)` | -| 6 — companion | `Greeter.forget() -> 100, then built=0`, `Greeter('x').built -> AttributeError (companion is class-only)` | -| 7 — fast path | `await g.greetNow(1) -> hello fast path!`, `Futures created -> 0` | -| 7 — really suspends | `await g.greetLater(2) -> hello slow path! hello slow path!`, `Futures created -> 1`, `raised -> None`, `completer thread -> clean` | +| 3, upcall | `handle 4294967327 -> 0 · with args -> presses x3 = 0` (called from Python via `_pm_resolve`/`_pm_invoke`) | +| 5, class proxy | `installed over PyMethodDef via JNI: 466 lines, 2 proxy classes`, `Greeter('Kotlin').greet(2) -> hello Kotlin! hello Kotlin!`, `g.greetings = 99 -> AttributeError (private set held)` | +| 6, companion | `Greeter.forget() -> 100, then built=0`, `Greeter('x').built -> AttributeError (companion is class-only)` | +| 7, fast path | `await g.greetNow(1) -> hello fast path!`, `Futures created -> 0` | +| 7, really suspends | `await g.greetLater(2) -> hello slow path! hello slow path!`, `Futures created -> 1`, `raised -> None`, `completer thread -> clean` | The last row resumes the parked continuation from a `java.lang.Thread`, as on desktop. (The original record adds that `pmp_attach` runs on that path too; a `java.lang.Thread` is already known to ART, so that is @@ -921,7 +924,7 @@ adds that `pmp_attach` runs on that path too; a `java.lang.Thread` is already kn `Worker` or memory-model decision in the sample; the library's `PythonProxyNativeDeliveryTest` does it with `pthread_create`). -**wasmJs** — the synchronous proxy surface runs (10 of `PythonProxyInstallTest`'s 11); `await` does not yet: +**wasmJs**, the synchronous proxy surface runs (10 of `PythonProxyInstallTest`'s 11); `await` does not yet: 1. *The trap is fixed.* `import selectors` / `import asyncio` used to kill the Node process with `RuntimeError: unreachable`. That was the third symptom: `select.poll().poll(0)` raised @@ -934,7 +937,7 @@ adds that `pmp_attach` runs on that path too; a `java.lang.Thread` is already kn flag to disable it); Gradle's runner uses Node 26. `selectors` was the first victim because it *calls* `poll(0)` at import time; `asyncio` died only through it. **Fix:** `cpython.mjs` (`python-multiplatform/src/wasmJsMain/resources/`) deletes `WebAssembly.promising` and - `WebAssembly.Suspending` before calling the Emscripten factory, putting Node 26 on the synchronous path — + `WebAssembly.Suspending` before calling the Emscripten factory, putting Node 26 on the synchronous path, which is what this library needs, since every call arrives from Kotlin as a synchronous wasm call. A `sys.modules['selectors']` shim was rejected: it only moves the death to the first loop iteration, hands users a fake module, and fixes the wrong layer. `wasmJsTest/.../WasmSelectorsImportTest.kt` (4 tests): @@ -968,8 +971,8 @@ more generated field (`FragmentScanner.traverseBody`, `ClassModel.traverseBody`) reflection-based binding cannot afford this, which is why mature ones document "do not create cycles" instead. Generated proxies reach the traversable type through the GC base of §4.2. -Status: SPEC M-3 — implemented on desktop (`desktopTest/.../ref/CycleCollectionTest.kt`, -`ksp-fixtures/app/.../RefHolderCycleCollectionTest.kt`), partial elsewhere. Mechanism and the hard parts +Status: SPEC M-3, implemented on desktop (`desktopTest/.../ref/CycleCollectionTest.kt`, +`python-multiplatform-ksp/fixtures/app/.../RefHolderCycleCollectionTest.kt`), partial elsewhere. Mechanism and the hard parts (traverse during collection, `tp_clear` mutating Kotlin state, cycles closing on the Kotlin side) are in [`object-lifetime.md`](object-lifetime.md). @@ -1029,11 +1032,11 @@ Controls from the same cut: The previous hand-cut round (warmup 100 000; five full suites per device, eleven on the hosts) read: desktop 510–560 ns, wasmJs 290–304 ns, iOS 2266–2301 ns, androidNative `pmp_api36` 3228–3275 ns, `pmp_api26` 3234–3310 ns, ART `pmp_api36` 944–1063 ns, ART `pmp_api26` 1146–1233 ns. (The predecessor document -labelled this list "warmup 3,000"; that was wrong — the 3 000-warmup desktop and wasmJs figures were 674 ns and +labelled this list "warmup 3,000"; that was wrong, the 3 000-warmup desktop and wasmJs figures were 674 ns and 322 ns.) In that round each host had exactly one outlying run in eleven, kept rather than dropped: desktop's GIL-held trampoline once read 186.81 ns against 76–91 ns (its ratio 2.89x against 5.62–7.02x); one wasmJs run inflated every boundary row together (upcall 432.55, downcall 144.64, trampoline 264.44 ns) while the empty GIL -scope stayed at 24.67 ns, so its ratio (2.99x) stayed in band — the case for preferring the ratio on wasm. +scope stayed at 24.67 ns, so its ratio (2.99x) stayed in band, the case for preferring the ratio on wasm. ### 7.2 Warmup: why 100 000, and what the old figures were measuring @@ -1052,7 +1055,7 @@ and ruled out CPython-side state are in [`downcall-design.md`](downcall-design.m 674 → 537 ns and wasmJs 322 → 292 ns. 13 desktop and 12 wasmJs full-suite runs, 3 of each with the lever pulled. -**JIT hosts fell, AOT hosts did not** — the prediction that separates the explanations: +**JIT hosts fell, AOT hosts did not**, the prediction that separates the explanations: | re-measured at 100 000 | upcall, old → new | downcall, old → new | |---|---|---| @@ -1065,7 +1068,7 @@ moved, 1209–1329 → 1146–1233 ns). ART has a JIT and moved like desktop and no warmup": from cold, androidNative's first 10 000 upcalls read ~1.3× the plateau; the old Kotlin/Native rows landed near it only because the rest of the suite had warmed the path. -**Device sweep** — 40 consecutive reps of 10 000 calls from cold; first-rep ratio = rep 1 / mean of reps 21–40: +**Device sweep**, 40 consecutive reps of 10 000 calls from cold; first-rep ratio = rep 1 / mean of reps 21–40: | Target | rep 1 / plateau (Python-driven upcall) | flat from | margin at 100 000 | |---|---|---|---| @@ -1073,10 +1076,10 @@ landed near it only because the rest of the suite had warmed the path. | androidNative `pmp_api36` | 1.27x | ~40 000 | 2.5x | | androidNative `pmp_api26` | 1.30x | ~30 000 | 3.3x | | ART `pmp_api26` | 1.93x | ~20 000, noisily | 5x | -| **ART `pmp_api36`** | **8.56x** | **~90 000–100 000** | **~1x — none** | +| **ART `pmp_api36`** | **8.56x** | **~90 000–100 000** | **~1x, none** | ART `pmp_api36`'s sweep: 9194.8, 3218.9, 2288.7, 1648.3, 1218.3, 1173.3, 1129.5, 1532.7, 1057.8, 1044.6 ns against -a plateau of 1074.2 — still 9% high at 70 000. Its downcall is flat from ~80 000 and its GIL-held trampoline +a plateau of 1074.2, still 9% high at 70 000. Its downcall is flat from ~80 000 and its GIL-held trampoline from ~50 000, so the Python-driven upcall sets the requirement. The controls are flat from the first rep on all five device configurations (iOS 10.5 → 10.2 ns and 37.9 → 38.0 ns; ART `pmp_api36` 17.3 → 17.3 ns and 49.7 → 50.4 ns), so the sweep measures the boundary, not the interpreter. The host sweeps behind `WARMUP`'s doc: @@ -1090,7 +1093,7 @@ ART `pmp_api36`, a slower device or busier host may read the boundary before it **Filtered runs.** A desktop `--tests` run of this class alone reads 506–550 ns, inside the full-suite band, and its controls match (8.4–8.6 vs 8.4 ns). **wasmJs is the exception**: filtered it reads 321–353 ns and its -*pure-Python* controls read 23.1 and 83.1 ns against the suite's 15.0 and 47.2 — CPython is itself a wasm +*pure-Python* controls read 23.1 and 83.1 ns against the suite's 15.0 and 47.2, CPython is itself a wasm module there, so a short-lived Node process runs the interpreter 55–74% slower. Tripling the warmup to 300 000 does not move it (host-lifetime-bound). wasmJs's ratios survive filtering (2.66–3.13x vs 2.91–3.14x); its absolutes are a full-suite figure only. The devices do not have the effect (three filtered runs vs five @@ -1116,7 +1119,7 @@ against a `threading.Thread` caller. pushes `test.kexe` and the CPython prefix (`Py_Initialize` aborts without a stdlib) to `/data/local/tmp`, runs it under the TeamCity logger and converts the service messages to JUnit XML under `build/test-results/androidNativeArm64Test/`, on every connected device with a matching ABI. First runs: 252 -tests, 0 failures, both emulators, five runs — `artMain` and the androidNative cinterop came up clean. +tests, 0 failures, both emulators, five runs, `artMain` and the androidNative cinterop came up clean. ### 7.3 The trampoline column needs a control @@ -1135,17 +1138,17 @@ simulator, 0.82–0.85x and 0.77–0.78x on androidNative `pmp_api36`/`pmp_api26 The **GIL-held** row is the comparable denominator (§7.1 uses it). Desktop and wasmJs never showed the effect because their GIL scope is 24–57 ns against 276–1221 ns on a device. At the old 3 000 warmup desktop's empty -scope read 124–152 ns and wasmJs's 79–81 ns — the cheapest operation is the one an unwarmed loop overstates +scope read 124–152 ns and wasmJs's 79–81 ns, the cheapest operation is the one an unwarmed loop overstates most. wasmJs's GIL-held trampoline is dearer than desktop's although its GIL scope is half the price, so wasm pays for the trampoline body, not the GIL. ### 7.4 Where the differences come from -- **iOS and androidNative: the scaffolding, not the boundary.** There is no runtime boundary on these targets — +- **iOS and androidNative: the scaffolding, not the boundary.** There is no runtime boundary on these targets, a `staticCFunction` in the same binary. What is expensive is per-call scaffolding every C API call shares: an empty `withPython` scope is 720–1002 ns there against 50–57 ns on desktop and 24–28 ns on wasm, and the same inflation hits the downcall denominator, which is why the ratio sits near 1.4. (In the round that measured - `Py_IncRef + Py_DecRef` — two calls, two GIL scopes — it cost 1551–2032 ns on iOS and 2010–2132 ns on + `Py_IncRef + Py_DecRef`, two calls, two GIL scopes, it cost 1551–2032 ns on iOS and 2010–2132 ns on androidNative against 110–120 ns on desktop and 44–49 ns on wasm; those device figures were unwarmed, so the multiple is not quotable, but the gap is wide enough that the conclusion survives.) Work to move either number belongs in the scaffolding, not in `UpcallEntry`. @@ -1153,13 +1156,13 @@ pays for the trampoline body, not the GIL. ns is the Panama upcall stub plus the `ctypes` shim, the largest relative boundary measured (5.62–7.02x in that round, 5.54–6.46x in the cost table). Desktop's `_pm_invoke` is a `ctypes.CFUNCTYPE` (and in this benchmark a Python `lambda *a: _pm_invoke(_pm_h, a)`), where other targets bind a `PyCFunction`, so desktop's row is an - **upper bound** on its boundary — it includes ctypes' argument conversion and, in the benchmark, one extra + **upper bound** on its boundary, it includes ctypes' argument conversion and, in the benchmark, one extra Python call (26.7–27.2 ns). -- **wasmJs: ~300 ns with arguments**, of which 122–129 ns is the trampoline; the rest is the Python-side callable +- **wasmJs: ~300 ns with arguments**: of which 122–129 ns is the trampoline; the rest is the Python-side callable and the crossing with a real tuple. Its scaffolding is the cheapest and most stable (24.47–27.52 ns for an empty scope across eleven runs), as there is no OS thread machinery; that row reads 34–37 ns when the class is run alone, for the reason in §7.2. -- **ART: one JNI upcall per call**, plus the per-thread attach the first time a CPython-created thread calls +- **ART: one JNI upcall per call**: plus the per-thread attach the first time a CPython-created thread calls (§7.6). Neither is on the marshalling path. ### 7.5 What a user calls: the generated proxy over the boundary @@ -1167,7 +1170,7 @@ pays for the trampoline body, not the GIL. `GeneratedProxyCostTest` (`commonTest`) prices each proxy surface against the raw `_pm_invoke` call it wraps, measured through the same object, in the same run, each row a zero-argument Python function called by one shared loop (the floor is subtracted). Each row is the **minimum of three timed loops**: the quantity is a -difference of tens of ns on a boundary of hundreds and noise is one-sided — with one loop per row, four runs put +difference of tens of ns on a boundary of hundreds and noise is one-sided, with one loop per row, four runs put the instance-method delta at −17, −31, −60 and −71 ns. The last row of each table is the report's own resolution: two raw rows are the same call (`_pm_invoke(h, ())`, different handle), so their difference is what cannot be resolved. Three runs; taken in the round before §7.2's warmup fix, so read internally (raw against @@ -1184,7 +1187,7 @@ proxy), not against §7.1's absolutes. | `Counter.created = 12` static write | 466–507 ns | 511–573 ns | 1.07–1.13x | +34…+66 ns | | `demo.calc.tally` top-level read | 449–485 ns | 484–521 ns | 1.03–1.07x | +14…+36 ns | | `demo.calc.tally = 9` top-level write | 466–507 ns | 520–553 ns | 1.08–1.11x | +44…+54 ns | -| `Counter(10)` constructor | 682–761 ns | 588–627 ns | — | not separable (below) | +| `Counter(10)` constructor | 682–761 ns | 588–627 ns | - | not separable (below) | | *resolution* | | | | *−3…+9 ns* | | **iOS simulator** (arm64) | | | | | | `demo.calc.ping()` module function | 1801–1890 ns | 1829–1895 ns | 1.00–1.01x | +6…+28 ns | @@ -1236,7 +1239,7 @@ an empty call: plain module attribute 14 ns; PEP 562 module `__getattr__` 214 ns | `demo.calc.tally = 9` write | +109…+136 ns (1.21–1.29x) | +44…+54 ns (1.08–1.11x) | `PythonProxyInstallTest` passed unchanged across it, including the read-only refusal; nothing in the source -says which shape is fifty times dearer — the measurement found it. +says which shape is fifty times dearer, the measurement found it. **The await fast path costs one coroutine frame:** @@ -1285,7 +1288,7 @@ Attach once per thread (after, commit `409da6fc`): | upcall / downcall of the same shape, worker | **0.99–1.09x** | **2.00–2.34x** | Re-run unchanged over five full suites per emulator after `09bf2397` put `commonTest` on the device (so -`UpcallBoundaryCostTest`'s several hundred thousand upcalls ran first and left the JIT warm — the observation the +`UpcallBoundaryCostTest`'s several hundred thousand upcalls ran first and left the JIT warm, the observation the warmup fix came from: the API 36 instrumentation row differs fourfold at the same warmup and emulator): | re-run, 5 suites each | API 26 | API 36 | @@ -1296,10 +1299,10 @@ warmup fix came from: the API 36 instrumentation row differs fourfold at the sam | first upcall on a fresh worker | 88 250–242 375 ns | 41 208–611 416 ns | On API 36 the claim sharpens (the gap is tight around zero). On API 26 a worker costs a little more (+43–249 ns -against a ~1250 ns call) — not a return of the per-call attach. **The first upcall on each worker still pays the +against a ~1250 ns call), not a return of the per-call attach. **The first upcall on each worker still pays the attach in full** (56–142 µs in the first round, 41–611 µs in the re-run): once per thread, not once per call. A worker that upcalls once gains nothing; one that upcalls in a loop is 20–50× better off. The structural assertion -the test enforces — one ART thread per Python worker — is a count and unaffected by warmup. +the test enforces, one ART thread per Python worker, is a count and unaffected by warmup. --- diff --git a/docs/guide/assets/style.css b/docs/guide/assets/style.css index bf63eb2ec..adb06056a 100644 --- a/docs/guide/assets/style.css +++ b/docs/guide/assets/style.css @@ -1,4 +1,4 @@ -/* python-multiplatform guide — shared thisisthepy tokens, accent #3b6cff */ +/* python-multiplatform guide, shared thisisthepy tokens, accent #3b6cff */ :root { --bg: #fbfbfa; --fg: #16181d; --muted: #5b6270; --line: #e6e6e3; --code-bg: #f3f3f1; --radius: 10px; --maxw: 72rem; diff --git a/docs/guide/concepts.html b/docs/guide/concepts.html index d45a88645..de25f7f0c 100644 --- a/docs/guide/concepts.html +++ b/docs/guide/concepts.html @@ -77,13 +77,13 @@

04ThreadsEvery C API call holds the GIL. Parallelism will come from free-threaded CPython (3.14t works on desktop with -PpythonFreeThreaded=true; the default is still the GIL), not from sub-interpreters: extensions that do not declare multi-interpreter support fail to import in a sub-interpreter.모든 C API 호출은 GIL 을 잡는다. 병렬성은 서브 인터프리터가 아니라 free-threaded CPython(desktop 에서 -PpythonFreeThreaded=true 로 3.14t 동작, 기본값은 여전히 GIL)에서 온다. 멀티 인터프리터 지원을 선언하지 않은 확장은 서브 인터프리터에서 import 가 실패하기 때문이다.

05Names이름

-

A Kotlin package is a Python module under the same name: androidx.compose.ui in Python is androidx. The binder never renames a namespace. Pythonic layers like pythonx are real Python packages that import those modules and reshape them — they live in their own repository.Kotlin 패키지는 같은 이름의 Python 모듈이다. Python 의 androidx.compose.ui 는 androidx 다. 바인더는 네임스페이스를 바꾸지 않는다. pythonx 같은 파이써닉 계층은 그 모듈을 가져와 다듬는 실제 Python 패키지이고, 자기 저장소에 있다.

+

A Kotlin package is a Python module under the same name: androidx.compose.ui in Python is androidx. The binder never renames a namespace. Pythonic layers like pythonx are real Python packages that import those modules and reshape them, they live in their own repository.Kotlin 패키지는 같은 이름의 Python 모듈이다. Python 의 androidx.compose.ui 는 androidx 다. 바인더는 네임스페이스를 바꾸지 않는다. pythonx 같은 파이써닉 계층은 그 모듈을 가져와 다듬는 실제 Python 패키지이고, 자기 저장소에 있다.

Design records for each topic are in docs/design.주제별 설계 기록은 docs/design 에 있다.

diff --git a/docs/guide/ecosystem.html b/docs/guide/ecosystem.html index ec5089fcd..87343bb0b 100644 --- a/docs/guide/ecosystem.html +++ b/docs/guide/ecosystem.html @@ -49,7 +49,7 @@

One boundary, several projects
- MIT licensed.MIT 라이선스. © 2024 thisisthepy + Apache-2.0 licensed.Apache-2.0 라이선스. © 2024 thisisthepy GitHub · Korean README한국어 README
diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index 2336f8197..61f3d1b07 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -72,15 +72,15 @@

03Your first call04Verify a change변경 검증하기

./gradlew :python-multiplatform:compileKotlinAndroidNativeArm64 --console=plain > .tmp/build.log 2>&1; echo "EXIT=$?"
 ./gradlew :python-multiplatform:desktopTest --rerun --console=plain > .tmp/test.log 2>&1; echo "EXIT=$?"
-./gradlew :ksp-fixtures:app:desktopTest --rerun
-./gradlew :ksp-fixtures:compose:desktopTest --rerun
-./gradlew :ksp-fixtures:artifact:desktopTest --rerun
+./gradlew :python-multiplatform-ksp:fixtures:app:desktopTest --rerun +./gradlew :python-multiplatform-compose:fixtures:compose:desktopTest --rerun +./gradlew :python-multiplatform-gradle-plugin-fixtures:artifact:desktopTest --rerun

Run the fixture modules one by one: a single invocation has hidden an ordering dependency before. Read exit codes from a file, never through a pipe.픽스처 모듈은 하나씩 따로 돌린다. 한 번에 돌리면 순서 의존성이 가려진 적이 있다. 종료 코드는 파이프가 아니라 파일에서 읽는다.

diff --git a/docs/guide/guide-binding-libraries.html b/docs/guide/guide-binding-libraries.html index cb52f2712..31378973a 100644 --- a/docs/guide/guide-binding-libraries.html +++ b/docs/guide/guide-binding-libraries.html @@ -56,7 +56,7 @@

02What you get diff --git a/docs/guide/guide-kotlin-to-python.html b/docs/guide/guide-kotlin-to-python.html index c3e73eb5c..6d63b8295 100644 --- a/docs/guide/guide-kotlin-to-python.html +++ b/docs/guide/guide-kotlin-to-python.html @@ -59,13 +59,13 @@

02Statements and expressions03Collections are Kotlin collections컬렉션은 Kotlin 컬렉션이다

PyList implements MutableList, PyDict implements MutableMap, PySet implements MutableSet, PyTuple implements List. Build them with fromList, fromMap, fromSet.PyList 는 MutableList, PyDict 는 MutableMap, PySet 은 MutableSet, PyTuple 은 List 를 구현한다. fromList, fromMap, fromSet 으로 만든다.

04Errors오류

-

A Python exception raised by exec or eval is thrown as PyException, carrying the real exception type and message. exec is built on PyRun_String rather than PyRun_SimpleString, which would print and clear the error before Kotlin could see it.exec 나 eval 에서 일어난 Python 예외는 실제 예외 타입과 메시지를 담은 PyException 으로 던져진다. exec 는 PyRun_SimpleString 이 아니라 PyRun_String 위에 있다 — 앞의 것은 Kotlin 이 보기 전에 오류를 출력하고 지워 버린다.

+

A Python exception raised by exec or eval is thrown as PyException, carrying the real exception type and message. exec is built on PyRun_String rather than PyRun_SimpleString, which would print and clear the error before Kotlin could see it.exec 나 eval 에서 일어난 Python 예외는 실제 예외 타입과 메시지를 담은 PyException 으로 던져진다. exec 는 PyRun_SimpleString 이 아니라 PyRun_String 위에 있다, 앞의 것은 Kotlin 이 보기 전에 오류를 출력하고 지워 버린다.

Specification: L-1, O-1 to O-7. Tests: Python3Test, PyListTest, PyExceptionTest.스펙: L-1, O-1 ~ O-7. 테스트: Python3Test, PyListTest, PyExceptionTest.

diff --git a/docs/guide/guide-platforms.html b/docs/guide/guide-platforms.html index c0d0f0441..b4406b500 100644 --- a/docs/guide/guide-platforms.html +++ b/docs/guide/guide-platforms.html @@ -56,7 +56,7 @@

04Web (experimental) diff --git a/docs/guide/guide-python-to-kotlin.html b/docs/guide/guide-python-to-kotlin.html index 24f76aa40..5d6459ddb 100644 --- a/docs/guide/guide-python-to-kotlin.html +++ b/docs/guide/guide-python-to-kotlin.html @@ -72,7 +72,7 @@

05Native images diff --git a/docs/guide/guides.html b/docs/guide/guides.html index 886a07113..75589bded 100644 --- a/docs/guide/guides.html +++ b/docs/guide/guides.html @@ -46,7 +46,7 @@

Task by task작업별로 diff --git a/docs/guide/index.html b/docs/guide/index.html index 579c48fd0..0d8b0c737 100644 --- a/docs/guide/index.html +++ b/docs/guide/index.html @@ -34,7 +34,7 @@
Kotlin Multiplatform × CPythonKotlin Multiplatform × CPython
-

Real CPython inside Kotlin — each language calls the other, on every target.Kotlin 안의 진짜 CPython — 두 언어가 서로를, 모든 타깃에서 부른다.

+

Real CPython inside Kotlin, each language calls the other, on every target.Kotlin 안의 진짜 CPython, 두 언어가 서로를, 모든 타깃에서 부른다.

python-multiplatform embeds the CPython interpreter itself in a Kotlin Multiplatform app. Kotlin sees Python as typed Kotlin objects; Python sees Kotlin classes as ordinary Python.python-multiplatform 은 CPython 인터프리터 그 자체를 Kotlin Multiplatform 앱에 임베딩한다. Kotlin 에게 Python 은 타입이 있는 Kotlin 객체이고, Python 에게 Kotlin 클래스는 평범한 Python 이다.

Get started시작하기 @@ -74,7 +74,7 @@

Philosophy철학

  • The real interpreter, not an imitation흉내가 아니라 진짜 인터프리터CPython itself is embedded, so C extensions and the whole Python ecosystem keep working.CPython 그 자체를 임베딩하므로 C 확장과 Python 생태계 전체가 그대로 동작한다.
  • Each language stays itself각 언어는 자기 모습 그대로A PyList is a Kotlin MutableList; a Kotlin class is constructed with Greeter('Kotlin'); an extension function is a method on its receiver.PyList 는 Kotlin 의 MutableList 이고, Kotlin 클래스는 Greeter('Kotlin') 으로 생성하며, 확장 함수는 수신 객체의 메서드다.
  • -
  • Built ahead of time, works in a closed world미리 생성하므로 닫힌 세계에서도 동작No runtime reflection. A function table generated at build time is why upcalls work on Kotlin/Native and in GraalVM native images — an upcall that passes only on the JVM is not done.런타임 리플렉션을 쓰지 않는다. 빌드 타임에 생성한 함수 테이블 덕분에 업콜이 Kotlin/Native 와 GraalVM 네이티브 이미지에서도 동작한다 — JVM 에서만 통과하는 업콜은 완료가 아니다.
  • +
  • Built ahead of time, works in a closed world미리 생성하므로 닫힌 세계에서도 동작No runtime reflection. A function table generated at build time is why upcalls work on Kotlin/Native and in GraalVM native images, an upcall that passes only on the JVM is not done.런타임 리플렉션을 쓰지 않는다. 빌드 타임에 생성한 함수 테이블 덕분에 업콜이 Kotlin/Native 와 GraalVM 네이티브 이미지에서도 동작한다, JVM 에서만 통과하는 업콜은 완료가 아니다.
  • Names are never rewritten이름은 바꾸지 않는다A Kotlin package is imported in Python under its own name. Pythonic layers such as pythonx are real Python packages built on top, in their own repository.Kotlin 패키지는 Python 에서 자기 이름 그대로 import 된다. pythonx 같은 파이써닉 계층은 그 위에 세운 실제 Python 패키지이며, 자기 저장소에 있다.
  • Every crossing is measured모든 경계 통과는 측정한다Each boundary mechanism ships with overhead tests; the cost table is rendered from real runs on every target.경계 메커니즘마다 오버헤드 테스트가 함께 있고, 비용 표는 모든 타깃의 실제 실행에서 렌더링된다.
  • @@ -93,14 +93,14 @@

    ContributingCheck that the change serves the project's intent (docs/INTENT.md on the develop branch).변경이 프로젝트의 의도(develop 브랜치의 docs/INTENT.md)에 맞는지 확인한다.
  • Describe the behaviour in the specification (docs/SPEC.md), marked planned.동작을 스펙(docs/SPEC.md)에 planned 로 적는다.
  • Write the test, run it, and include the red output in the pull request.테스트를 쓰고 돌려서, 빨간 출력을 풀 리퀘스트에 함께 올린다.
  • -
  • Implement; include the androidNative compile and each ksp-fixtures module, run separately, in your verification.구현한다. 검증에는 androidNative 컴파일과 ksp-fixtures 모듈 각각(따로 실행)을 포함한다.
  • +
  • Implement; include the androidNative compile and each fixtures/ module, run separately, in your verification.구현한다. 검증에는 androidNative 컴파일과 fixtures/ 모듈 각각(따로 실행)을 포함한다.
  • Work on a work/<topic> branch and open the pull request against develop.work/<topic> 브랜치에서 작업하고 develop 을 대상으로 풀 리퀘스트를 연다.

    diff --git a/docs/guide/status.html b/docs/guide/status.html index c171fcf1c..d528808c4 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -71,7 +71,7 @@

    Why is androidx not renamed to something P diff --git a/docs/investigations/android-unregistered-surface.md b/docs/investigations/android-unregistered-surface.md index 26ce36956..b080238f1 100644 --- a/docs/investigations/android-unregistered-surface.md +++ b/docs/investigations/android-unregistered-surface.md @@ -21,7 +21,7 @@ Kotlin/Native `String` parameter, or a boxed `java.lang.Long`. `everyDeclarationIsRegisteredOrIsOneOfTheFourNamedExceptions` parses `bindings.kt` and `jni_onload.def` on every desktop build and fails if a declaration has no entry or the exception set above goes stale. The rule is written in `androidMain/README.md` ("Every `external fun` here -needs a table entry — no exceptions by reachability"). The migration this report recommended +needs a table entry, no exceptions by reachability"). The migration this report recommended (strings first, then core object model, then exceptions, then the rest) is complete. ## Numbers (all historical) diff --git a/docs/investigations/cost-table.md b/docs/investigations/cost-table.md index ed9b30d15..adf65b5b1 100644 --- a/docs/investigations/cost-table.md +++ b/docs/investigations/cost-table.md @@ -2,16 +2,16 @@ Kotlin ↔ Python 경계의 비용을, **여섯 타깃 전부에 대해 한 번에** 뽑아 놓은 표. -> **결론과 상태 (2026-10-03 확인).** 업콜 1건은 같은 모양의 다운콜의 약 0.9~3.1 배다 — 호스트 JVM 에서 +> **결론과 상태 (2026-10-03 확인).** 업콜 1건은 같은 모양의 다운콜의 약 0.9~3.1 배다, 호스트 JVM 에서 > 약 543–576 ns (다운콜 260–266), iOS 시뮬레이터 약 2.3 µs, androidNative 약 3.4 µs, ART API 26/36 약 1.0–1.3 µs, > wasmJs 약 300 ns. 모든 숫자는 아래 마커 사이의 표(커밋 `958c0082b294`, 2026-08-14, Apple M1, 타깃당 3회의 > min–max, 워밍업 100,000)에서 온 것이고, 그 표의 숫자는 이 문장보다 우선한다. 하네스(`benchmarks/cost-table.sh`, -> `benchmarks/cost_table.py`)는 이 문서의 마커 사이만 다시 쓴다 — 마커와 그 바깥의 "다시 뽑는 법" 절은 하네스가 +> `benchmarks/cost_table.py`)는 이 문서의 마커 사이만 다시 쓴다, 마커와 그 바깥의 "다시 뽑는 법" 절은 하네스가 > 기대하는 형식이므로 손으로 고치지 않는다. 이 표는 **측정을 기록할 뿐 임계값을 단언하지 않는다**(SPEC X-1). > 표는 2026-08-14 컷이므로 그 뒤의 커밋을 반영하지 않는다. 다시 뽑으려면 아래 "다시 뽑는 법". `docs/design/upcall.md` 와 `docs/design/downcall-design.md` 는 각자의 조사 과정에서 나온 표를 갖고 있고, -그 표들은 **조사가 끝난 시점의 기록**이다. 이 문서는 다르다 — 여기 있는 표는 사람이 옮겨 적은 것이 +그 표들은 **조사가 끝난 시점의 기록**이다. 이 문서는 다르다, 여기 있는 표는 사람이 옮겨 적은 것이 아니라 `benchmarks/cost_table.py` 가 실행 결과에서 직접 렌더링한 것이고, 아래 "다시 뽑는 법" 한 줄이면 통째로 갱신된다. @@ -40,7 +40,7 @@ ANDROID_HOME=/Users/ibrew/Library/Android/sdk ./benchmarks/cost-table.sh --runs | 조건 | 없으면 | |---|---| -| **기계가 조용할 것** — 1분 load average 가 4.0 미만 | 수집이 **거부된다.** `--allow-busy` 로 넘길 수 있으나, 그 경우 산출물과 표 머리에 오염 표시가 붙고 그 숫자는 인용 대상이 아니다 | +| **기계가 조용할 것**, 1분 load average 가 4.0 미만 | 수집이 **거부된다.** `--allow-busy` 로 넘길 수 있으나, 그 경우 산출물과 표 머리에 오염 표시가 붙고 그 숫자는 인용 대상이 아니다 | | `ANDROID_HOME` 이 platform-tools 가 있는 SDK 를 가리킬 것 | ART 두 타깃과 `androidNativeArm64` 가 **skipped 로 기록된다** (조용히 빠지지 않는다) | | 에뮬레이터 `pmp_api26`, `pmp_api36` 이 떠 있을 것 | 해당 타깃이 skipped 로 기록되고, 띄우는 명령이 출력과 JSON 에 함께 남는다 | | Xcode 라이선스 동의 | `iosSimulatorArm64` 가 실패로 기록된다 | @@ -67,7 +67,7 @@ python3 benchmarks/cost_table.py render --check # 문서가 최신 JSO ``` `--targets` 로 좁히는 것은 **타깃**을 좁히는 것이지 스위트를 좁히는 것이 아니다. 하네스는 어떤 -경우에도 `--tests` 를 쓰지 않는다 — 스위트를 좁히면 측정 영역 자체가 바뀌고, 이 저장소는 그것으로 +경우에도 `--tests` 를 쓰지 않는다, 스위트를 좁히면 측정 영역 자체가 바뀌고, 이 저장소는 그것으로 한 회차를 버렸다 (양 끝이 1300~1400 ns 로 올라갔다). ### 나눠 뜬 것을 하나의 표로 합치기 @@ -100,10 +100,10 @@ python3 benchmarks/cost_table.py render --in benchmarks/results/host.json,benchm | `artApi26` | `:python-multiplatform:connectedDebugAndroidTest` | `build/outputs/androidTest-results/connected/debug/<기기>/logcat-*.txt` | **에뮬레이터** (`pmp_api26`) | | `artApi36` | `:python-multiplatform:connectedDebugAndroidTest` | 〃 | **에뮬레이터** (`pmp_api36`) | -기본 여섯 외에 `androidNativeArm64Api26` 이 하나 더 선언되어 있다 — `docs/design/upcall.md` 의 표가 +기본 여섯 외에 `androidNativeArm64Api26` 이 하나 더 선언되어 있다, `docs/design/upcall.md` 의 표가 androidNative 를 API 레벨별로 나눠 기록하기 때문이고, `--targets` 로 이름을 대면 돈다. -**ART 만 결과를 읽는 경로가 다르다.** AGP 가 쓰는 JUnit XML 에는 `` 이 없다 — +**ART 만 결과를 읽는 경로가 다르다.** AGP 가 쓰는 JUnit XML 에는 `` 이 없다, 251개 testcase 에 0개였다. 대신 AGP 는 테스트별 `logcat-<클래스>-<메서드>.txt` 를 옆에 남기고, 벤치마크의 `println` 은 거기 `I System.out:` 줄로 들어간다. 하네스는 그 줄에서 접두사를 떼고 읽는다. @@ -113,7 +113,7 @@ androidNative 를 API 레벨별로 나눠 기록하기 때문이고, `--targets` | 파일 | 소스셋 | 재는 것 | 파서가 읽는 형태 | |---|---|---|---| -| `overhead/Benchmark.kt` | `commonTest` | 하네스 자체 (`run`/`measure`) — 벤치마크가 아니다 | — | +| `overhead/Benchmark.kt` | `commonTest` | 하네스 자체 (`run`/`measure`), 벤치마크가 아니다 | - | | `overhead/BenchmarkTest.kt` | `commonTest` | 포인터 박싱, refcount 왕복, 문자열 마샬링(8/256/8192자), 정수 마샬링, `PyObject` 래퍼, `PyObject_GetAttrString` | `--- Benchmark Report ---` 표 | | `native/ffi/UpcallBoundaryCostTest.kt` | `commonTest` | **업콜 1건**과 그것을 재는 데 필요한 다섯 기준선(같은 실행 안에서) | `--- Upcall boundary cost: <플랫폼> ---` 블록 | | `ffi/upcall/GeneratedProxyCostTest.kt` | `commonTest` | 생성된 프록시 계층이 붙이는 비용 | `--- Generated proxy cost... ---` 블록 (JSON·TSV 에는 들어가나 아래 표에는 쓰지 않는다) | @@ -126,14 +126,14 @@ androidNative 를 API 레벨별로 나눠 기록하기 때문이고, `--targets` **다운콜은** `BenchmarkTest` 와 `UpcallBoundaryCostTest.measureDowncalls()` 에서, **업콜은** `UpcallBoundaryCostTest` 와 `reflection/UpcallOverheadTest` 에서 나온다. -아래 표 두 개는 앞의 둘만 쓴다 — 여섯 타깃 전부에서 같은 모양으로 도는 것이 그 둘이기 때문이다. +아래 표 두 개는 앞의 둘만 쓴다, 여섯 타깃 전부에서 같은 모양으로 도는 것이 그 둘이기 때문이다. 파서는 라벨을 하드코딩하지 않는다. 위 블록 형식(`--- 제목: 플랫폼 ---`, 조건 줄, 들여쓴 `라벨 값 ns`) 을 따르는 것은 전부 JSON·TSV 로 들어간다. 표에 어떤 라벨을 쓸지는 렌더러가 고른다. ### 워밍업은 타깃의 성질이 아니라 **테스트 클래스**의 성질이다 -`Benchmark.printReport` 로 출력하는 클래스가 여럿이고, 각자 워밍업이 다르다 — +`Benchmark.printReport` 로 출력하는 클래스가 여럿이고, 각자 워밍업이 다르다, `BenchmarkTest` 는 100,000, `reflection/UpcallOverheadTest` 는 `Benchmark.run` 의 기본값 1,000, `GeneratedProxyCostTest` 는 5,000. 그래서 하네스는 모든 행에 **출력한 테스트 클래스**를 함께 기록하고, 렌더러는 `BenchmarkTest` 행만 그 표에 넣는다. 나머지는 "출력한 테스트" 열을 달아 별도 @@ -152,7 +152,7 @@ androidNative 를 API 레벨별로 나눠 기록하기 때문이고, `--targets` 공백 구분자를 요구하는 파서는 **다운콜 표를 이루는 다섯 행을 통째로 놓친다.** 하네스는 값을 뒤쪽 단위(`ns`)에 앵커해 읽으므로 영향받지 않는다. 다만 사람이 읽는 출력으로서는 결함이고, -`commonTest` 파일을 고치는 일이라 여기서는 손대지 않고 기록만 해 둔다 — 고칠 때는 `padEnd(54)` 를 +`commonTest` 파일을 고치는 일이라 여기서는 손대지 않고 기록만 해 둔다, 고칠 때는 `padEnd(54)` 를 가장 긴 라벨(58자)보다 크게 올리면 된다. --- @@ -181,7 +181,7 @@ androidNative 를 API 레벨별로 나눠 기록하기 때문이고, `--targets` | 파일 | 내용 | |---|---| -| `benchmarks/results/cost-table-<시각>.json` | 실행별 전체 기록 — 조건, 스위트 집계, 파싱된 모든 지표, 건너뛴 타깃과 그 이유 | +| `benchmarks/results/cost-table-<시각>.json` | 실행별 전체 기록, 조건, 스위트 집계, 파싱된 모든 지표, 건너뛴 타깃과 그 이유 | | `benchmarks/results/cost-table-<시각>.tsv` | 같은 것의 long-format. `target run status source section label value unit iterations platform commit contaminated` | | `benchmarks/results/logs/<타깃>-run-<시각>.log` | 그 실행의 Gradle 출력 전체 | | 이 문서의 마커 사이 | 위 JSON 에서 렌더링된 표 | @@ -192,7 +192,7 @@ CPU·코어 수·OS, **시작/종료 load average 와 ceiling**, 타깃당 실 `WARMUP_BULK`, **기기·시뮬레이터 식별자**(AVD 이름, 시리얼, API 레벨, ABI / 부팅된 시뮬레이터), 플랫폼이 스스로 보고한 이름, 스위트 통과·실패·스킵 수, 벽시계 시간, 그리고 태스크 종료 코드. -`BenchmarkTest` 의 워밍업만 소스에서 읽는다 — 그 테스트는 자기 워밍업을 출력하지 않기 때문이다. +`BenchmarkTest` 의 워밍업만 소스에서 읽는다, 그 테스트는 자기 워밍업을 출력하지 않기 때문이다. (`UpcallBoundaryCostTest` 는 출력한다.) 워밍업 없는 숫자가 이 표를 한 번 무효화했으므로 추정하지 않고, 읽지 못하면 그 사실을 기록한다. @@ -206,17 +206,17 @@ _Generated by `benchmarks/cost_table.py` from `cut-hostless.json, cut-api36.json Every number below is a function of these. A cost table without them is a record of whatever else was running. -- **commit** `958c0082b294` — Merge branch 'work/objecttype' into develop +- **commit** `958c0082b294`, Merge branch 'work/objecttype' into develop - **branch** `develop`, worktree `/Volumes/macMini/thisisthepy/PythonMultiplatform` -- **captured** 2026-08-14T16:31:30+09:00 — the first of several cuts; each one's own time is in the source table below +- **captured** 2026-08-14T16:31:30+09:00, the first of several cuts; each one's own time is in the source table below - **host** Apple M1, 8 cores, Darwin 25.5.0 arm64 -- **load average** [1.97, 2.29, 5.92] at start, [2.47, 3.28, 5.19] at end (ceiling 4.0, guard `ok`) — of the first cut only; per-cut figures below -- **runs per target** 3 — ranges below are min–max over them +- **load average** [1.97, 2.29, 5.92] at start, [2.47, 3.28, 5.19] at end (ceiling 4.0, guard `ok`), of the first cut only; per-cut figures below +- **runs per target** 3, ranges below are min–max over them - **`UpcallBoundaryCostTest`** warmup and iteration counts are self-reported per target in the table below (the test prints them) -- **`BenchmarkTest`** warmup, read from the source at this commit: `WARMUP` = 100000, `WARMUP_BULK` = 10000 (the ≥4096-char string rows use the second — see that file's own caveat) +- **`BenchmarkTest`** warmup, read from the source at this commit: `WARMUP` = 100000, `WARMUP_BULK` = 10000 (the ≥4096-char string rows use the second, see that file's own caveat) - **suite scope** full task, never `--tests`-filtered: narrowing the suite changes what is being measured -This table is merged from several cuts — which target came from which: +This table is merged from several cuts, which target came from which: | cut | commit | captured | load at start | contaminated | targets | |---|---|---|---|---|---| @@ -226,9 +226,9 @@ This table is merged from several cuts — which target came from which: | target | task | status | device / simulator | platform, self-reported | tests/fail/skip | wall s | loop counts | |---|---|---|---|---|---|---|---| -| `desktop` | `:python-multiplatform:desktopTest` | ok | — | MacOS 26.5.1 (aarch64) / JVM 21.0.12 | 396/0/1 | 30.0/12.7/12.5 | iterations per loop: 10000, warmup: 100000 | -| `iosSimulatorArm64` | `:python-multiplatform:iosSimulatorArm64Test` | ok | — | iOS 26.2 (SDK 260200, arm64) / Native | 350/0/0 | 66.1/36.7/36.5 | iterations per loop: 10000, warmup: 100000 | -| `wasmJs` | `:python-multiplatform:wasmJsNodeTest` | ok | — | Web emscripten (wasm32) / Wasm Kotlin/Wasm | 364/0/0 | 21.3/5.9/5.7 | iterations per loop: 10000, warmup: 100000 | +| `desktop` | `:python-multiplatform:desktopTest` | ok | - | MacOS 26.5.1 (aarch64) / JVM 21.0.12 | 396/0/1 | 30.0/12.7/12.5 | iterations per loop: 10000, warmup: 100000 | +| `iosSimulatorArm64` | `:python-multiplatform:iosSimulatorArm64Test` | ok | - | iOS 26.2 (SDK 260200, arm64) / Native | 350/0/0 | 66.1/36.7/36.5 | iterations per loop: 10000, warmup: 100000 | +| `wasmJs` | `:python-multiplatform:wasmJsNodeTest` | ok | - | Web emscripten (wasm32) / Wasm Kotlin/Wasm | 364/0/0 | 21.3/5.9/5.7 | iterations per loop: 10000, warmup: 100000 | | `androidNativeArm64` | `:python-multiplatform:androidNativeArm64Test` | ok | `pmp_api36` (emulator-5554, API 36, arm64-v8a) | Android 16 (SDK 36, aarch64) / Native | 353/0/0 | 76.4/46.7/46.3 | iterations per loop: 10000, warmup: 100000 | | `artApi26` | `:python-multiplatform:connectedDebugAndroidTest` | failed | `pmp_api26` (emulator-5556, API 26, arm64-v8a) | Android 8.0.0 (SDK 26, aarch64) / ART VM 0.9 | 381/3/0 | 153.8/149.1/148.3 | iterations per loop: 10000, warmup: 100000 | | `artApi36` | `:python-multiplatform:connectedDebugAndroidTest` | failed | `pmp_api36` (emulator-5554, API 36, arm64-v8a) | Android 16 (SDK 36, aarch64) / ART VM 0.9 | 381/2/0 | 262.9/148.7/146.7 | iterations per loop: 10000, warmup: 100000 | @@ -246,7 +246,7 @@ This table is merged from several cuts — which target came from which: | `artApi26` | 1196.52–1272.28 ns | 1253.41–1295.79 ns | 1800.86–1863.97 ns | 0.92–1.01x | 1.05–1.18x | | `artApi36` | 995.61–1154.10 ns | 804.27–838.60 ns | 1100.87–1167.17 ns | 1.18–1.43x | 1.10–1.29x | -Controls, from the same block — these are what say whether a row moved because the boundary moved or because the whole host did: +Controls, from the same block, these are what say whether a row moved because the boundary moved or because the whole host did: | target | empty Python loop | pure-Python callee | GIL-held trampoline | |---|---|---|---| @@ -306,22 +306,22 @@ These also come through `Benchmark.printReport`, and they are **not comparable w | `UpcallOverheadTest` | `arrayOf(1L) allocation + boxing` | 1,000,000 | 6.44–6.64 ns | 67.25–68.87 ns | 8.66–8.98 ns | 76.93–77.80 ns | 9.31–10.23 ns | 18.86–20.10 ns | | `UpcallOverheadTest` | `invoke with a pre-built args array` | 1,000,000 | 8.64–8.91 ns | 173.09–179.16 ns | 23.07–24.06 ns | 240.92–251.28 ns | 34.76–37.70 ns | 60.44–61.46 ns | | `UpcallOverheadTest` | `direct Kotlin call (floor)` | 1,000,000 | 2.26–2.95 ns | 15.02–15.39 ns | 4.45–4.63 ns | 15.15–15.49 ns | 4.14–4.21 ns | 13.79–14.69 ns | -| `WasmFinalizationTest` | `cleaner: registerCleaner + close (the whole hook)` | 50,000 | — | — | 77.70–90.21 ns | — | — | — | -| `WasmFinalizationTest` | `cleaner: toJsReference() alone (the externref crossing)` | 50,000 | — | — | 32.70–35.38 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal: malloc+encodeToByteArray+copy+free (old)` | 200,000 | — | — | 76.95–80.44 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal: malloc+free only` | 200,000 | — | — | 13.41–14.15 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal: encodeToByteArray only` | 200,000 | — | — | 50.10–52.39 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal: Wasm.scratchUtf8` | 200,000 | — | — | 28.44–29.48 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal: Wasm.internedUtf8 (hit)` | 200,000 | — | — | 20.71–21.71 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal 54-char name: Wasm.scratchUtf8` | 200,000 | — | — | 145.61–152.15 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal 54-char name: Wasm.internedUtf8 (hit)` | 200,000 | — | — | 24.73–25.88 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `getattr: C string allocated per call (old)` | 50,000 | — | — | 241.35–259.20 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `getattr: interned C string (new)` | 50,000 | — | — | 186.85–200.06 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `getattr via actual: C string per call (old)` | 50,000 | — | — | 222.20–239.95 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `getattr via actual: interned (new)` | 50,000 | — | — | 191.19–205.26 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `getattr: scratch C string` | 50,000 | — | — | 192.62–204.75 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal 4000 B: malloc+encode+copy+free (old)` | 20,000 | — | — | 14285.62–14866.91 ns | — | — | — | -| `WasmMarshallingOverheadTest` | `marshal 4000 B: Wasm.scratchUtf8` | 20,000 | — | — | 9420.60–9830.11 ns | — | — | — | +| `WasmFinalizationTest` | `cleaner: registerCleaner + close (the whole hook)` | 50,000 | - | - | 77.70–90.21 ns | - | - | - | +| `WasmFinalizationTest` | `cleaner: toJsReference() alone (the externref crossing)` | 50,000 | - | - | 32.70–35.38 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal: malloc+encodeToByteArray+copy+free (old)` | 200,000 | - | - | 76.95–80.44 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal: malloc+free only` | 200,000 | - | - | 13.41–14.15 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal: encodeToByteArray only` | 200,000 | - | - | 50.10–52.39 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal: Wasm.scratchUtf8` | 200,000 | - | - | 28.44–29.48 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal: Wasm.internedUtf8 (hit)` | 200,000 | - | - | 20.71–21.71 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal 54-char name: Wasm.scratchUtf8` | 200,000 | - | - | 145.61–152.15 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal 54-char name: Wasm.internedUtf8 (hit)` | 200,000 | - | - | 24.73–25.88 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `getattr: C string allocated per call (old)` | 50,000 | - | - | 241.35–259.20 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `getattr: interned C string (new)` | 50,000 | - | - | 186.85–200.06 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `getattr via actual: C string per call (old)` | 50,000 | - | - | 222.20–239.95 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `getattr via actual: interned (new)` | 50,000 | - | - | 191.19–205.26 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `getattr: scratch C string` | 50,000 | - | - | 192.62–204.75 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal 4000 B: malloc+encode+copy+free (old)` | 20,000 | - | - | 14285.62–14866.91 ns | - | - | - | +| `WasmMarshallingOverheadTest` | `marshal 4000 B: Wasm.scratchUtf8` | 20,000 | - | - | 9420.60–9830.11 ns | - | - | - | diff --git a/docs/investigations/fork-desktop-paths-macos.md b/docs/investigations/fork-desktop-paths-macos.md new file mode 100644 index 000000000..bbdc507c1 --- /dev/null +++ b/docs/investigations/fork-desktop-paths-macos.md @@ -0,0 +1,129 @@ +# The fork's desktop paths on macOS (issue #191, investigation #242) + +What python-multiplatform needs before a pythonx-compose app runs on the two desktop paths +`compose-multiplatform-core-extended` and `compose-multiplatform-extended` add: a GraalVM native +image and a Kotlin/Native executable, macOS arm64 first. Read on 2026-10-17 from the fork's +`origin/extended` branches (core `61906995435`, plugin fork `fca1473a`), `~/.m2/repository`, and +this repository's `develop` (`af4debac`). Nothing here was built; every fact cites where it was read. + +## 1. What the fork has on macOS + +| | GraalVM native image | Kotlin/Native | +|---|---|---| +| Fork's claim | `extended/README.md` (core, line 12): "macOS works on both: the Kotlin/Native window (AppKit and Metal) and the GraalVM native-image window." | same line | +| Window layer | `extended/window/graalvm/graalvm-macos` (JVM lib, AppKit through `@CFunction`, `upcalls.txt` table, no reflection) | `extended/window/native/macos` (`macosArm64`: AppKit, Metal, `MetalSurface.kt`) | +| Compose host | **none for this path yet**: `extended/window/scene` (JVM and `macosArm64`) hosts a Compose scene through `runComposeWindow(platform, config, surfaceFor, content)` (`scene/src/.../ComposeWindow.kt:285`), but `graalvm-macos` provides no `WindowSurface` to draw into | `scene` with `native/macos`'s `MetalSurface` | +| Measured with Compose content | `extended/native-image/hello` (plugin fork): Material 3 in an **AWT** window (`windowing = Awt`), 82 MB, "draws the same content through Skia offscreen into a PNG byte for byte the one the JVM run draws" (`extended/native-image/README.md`, 2026-10-02) | no Gradle demo in the fork; compose-rust's Amper renderer (`darkpyonix/compose-rust/renderer/macos`) consumes `native-macos` | +| AWT-free demo | `extended/native-image/hello-awtfree-link`: opens, pumps and presents an AppKit window; **draws no Compose** | none | +| Build | the plugin fork's `org.jetbrains.compose` `1.11.1-extended-dev`: `compose.desktop.application { nativeImage { graalvmHome; skikoStaticDirectory } }`, `packageNativeImage`, `runNativeImageAgent`; skiko as a static archive (`extended/skiko/build-skiko-static-jvm.sh`) | `extended/gradle-plugin-kn`: `createDistributableNative...`, `runNative...`, `packageKotlinNative` | + +**Published to mavenLocal** (JetBrains coordinates kept, as #191 says): + +- `org.jetbrains.compose:ui*`, `foundation*`, `ui-text*` at `1.11.1001` (2026-10-17), including the + `macosarm64` klibs of `ui` and `foundation`. No `material3` macOS klib. +- `org.jetbrains.compose:compose-gradle-plugin:1.11.1-extended-dev`. +- `org.thisisthepy.compose.window:*:0.1.0`: `common`, `graalvm-macos`, `graalvm-linux`, `native-macos`. Not + `scene`, and not `native-linux`. +- `org.jetbrains.skiko:skiko-awt:0.144.6.1`. +- No publish script producing `1.11.1001` was found in either fork repository; the "Extended window + publish" workflow and compose-rust's `scripts/publish-window.sh` publish the window modules. + +## 2. What this repository has + +- **Targets.** `python-multiplatform` declares `jvm("desktop")`, Android, iOS, androidNative and wasm; + no `macosArm64`, `linuxX64` or `mingwX64` (`python-multiplatform/build.gradle.kts`; the plugin's + `hostDesktopPlatform` knows macOS only as a JVM host). The sample has the same set. +- **A native image already runs Python** (`docs/platforms/graal-native-image-verification.md`, + 2026-08-17, `4687070a`): `:sample:nativeCompile` (an `Exec` of Liberica NIK 25.0.4's `native-image`, + `--no-fallback --enable-native-access=ALL-UNNAMED`) builds a headless `NativeImageMainKt`; 28 + downcalls and 5 upcalls registered, `UPCALL_OK`/`PROXY_OK`, no `libjvm`, `libpython` `dlopen`ed. + It is **headless on purpose**: the sample's `application {}` needs AWT. Not automated, not re-run. +- **The upcall table on a native image.** Desktop upcalls are FFM (`Panama.kt`), registered by the + generated `META-INF/native-image/org.thisisthepy/python-multiplatform/reachability-metadata.json` + (`generateDesktopReachabilityMetadata`) plus `native-image/reachability-metadata.base.json`. That + metadata ships in the jar, so any native-image build that has the jar on its classpath picks it up. + (The verification note says no reflection registration is needed, while its own log shows 1,822 + types registered and the base file registers `UpcallTarget`; the note should be corrected.) +- **PYTHONHOME.** `stagePythonHome` sets `PYTHONHOME` for `JavaExec`/`Test` tasks and copies the prefix + into Compose's `prepare*AppResources`; the native demo is given `PYTHONHOME` by its run task. A + native image has no `java.home` and no packaged-resources directory, so nothing stages a prefix + **beside the executable** or finds it from there at start. +- **The notebook E2E** (`python-multiplatform-compose/fixtures/notebook-e2e`) needs no window: it + draws `PythonAppView` into an offscreen `ImageComposeScene` (`NotebookHost.kt:132`) and compares + pixels. It is a JVM `desktopTest` suite with `compose.desktop.currentOs` (Skia natives) and + `java.awt.headless=true`; no `java.awt`/`javax.swing` import. + +## 3. What each path needs from this side + +### GraalVM native image + +1. **A desktop app built by the plugin fork's `packageNativeImage`** rather than the sample's hand-made + `Exec`: the fork already solves Compose's own native-image configuration and static skiko. The + generated upcall table and FFM registrations come along in the jar's `META-INF/native-image`, so + **the upcall table carries over unchanged** (build-time table, no reflection: AGENTS.md §12.3, §13). + To check: that the fork's plugin passes `--enable-native-access=ALL-UNNAMED` (or lets an app add it) + and does not set `--no-fallback`-incompatible options that hide a missing FFM registration. +2. **PYTHONHOME beside the executable.** A task that stages the prefix and the installed wheels next to + `build/compose/native-image/main/`, and a runtime lookup (`ProcessHandle`'s executable path, a + sibling `python/` directory) when `PYTHONHOME` is unset, like `IosPythonHome` does for an app bundle. + **Staging does not carry over as is**: today's paths are a Gradle task's environment and the JVM + packaging's resources directory. +3. **The walker over the fork's jars.** Same public API, so the walk must produce the same bindings; + guarded by the stub diff of pythonx-compose #54. +4. **Selecting the path.** A Gradle plugin option (upstream Compose by default, the fork's native image + as a choice), #191's first bullet. + +### Kotlin/Native + +1. **An interpreter target**: `macosArm64` in `python-multiplatform` (cinterop against the macOS + python-build-standalone prefix the desktop JVM already downloads, a static or shipped + `libpython`), with the native `HandleTable`/trampoline path iOS and androidNative already use. + This is new work, not a switch. +2. **The klib walker** (B-2) over the fork's `macosarm64` klibs: since #234/#237 it carries handles and + interface members, but not constructors, properties or composables, and Compose's surface is mostly + composables and extensions; the K/N path needs the walker to reach about where the jar walker is. +3. **PYTHONHOME** beside the executable, as for the native image. +4. `material3` for `macosArm64`: no fork klib is published (the fork says only `ui` and `foundation` + differ from JetBrains; JetBrains' own `material3` macOS klib would have to resolve beside them). + +## 4. First milestone: the notebook E2E in a macOS GraalVM native image + +The E2E draws offscreen, so the milestone needs **no window** and none of the AWT-free pieces: + +1. A `notebook-e2e` native-image entry point (a `main` running the existing scenarios and printing a + pass/fail line per scenario, the way the fork's `hello` self-checks through `HELLO_SELF_CHECK`), + because a JUnit `desktopTest` does not run inside an image. +2. Built with the plugin fork's `packageNativeImage` (`windowing = Awt`, which is what the fork measured; + the scenes are offscreen either way), `--enable-native-access=ALL-UNNAMED`, the fork's static skiko. +3. PYTHONHOME and the pythonx-compose wheel staged beside the executable (§3, GraalVM 2). +4. Pass: the same 20 scenarios as the JVM run (pixel comparisons included), the binary run alone from + an empty directory with `JAVA_HOME` unset, and `otool -L` showing no `libjvm`. Then a CI job on a + macOS runner, as `notebook-e2e` has on the JVM (#213). + +Milestone 2 is the same app in the AWT-free window, once the fork has the pieces in §5. Kotlin/Native +follows the interpreter target (§3, K/N 1). + +## 5. Requests for the fork (through the darkpyonix lead) + +1. **Publish `org.thisisthepy.compose.window:scene`** (its `module.yaml` already enables publishing; + it is missing from mavenLocal and from the "Extended window publish" workflow's output). +2. **A `WindowSurface` for `graalvm-macos`** (Metal or GL into a Skia surface, as `native/macos`'s + `MetalSurface`), so `runComposeWindow` can host Compose content on the GraalVM path; and a demo that + draws Compose through it (`hello-awtfree-link` draws nothing today). +3. **A documented, reproducible publish of `1.11.1001`** (no script producing that version was found) + and one version line for the Compose libraries, the window modules and the Gradle plugin, so a + consumer pins one set. +4. **The native-image flags an app can add**: confirm that `packageNativeImage` lets an app pass + `--enable-native-access=ALL-UNNAMED` and merges a library's `META-INF/native-image` metadata (FFM + `foreign` registrations included). +5. **A hook to stage extra files beside the image** (a prefix directory), or confirmation that a + consumer task after `packageNativeImage` is the supported way. +6. For Kotlin/Native: whether `material3` for `macosArm64` is expected to come from JetBrains, and a + Gradle (not Amper-only) example of `scene` plus `native-macos` hosting Compose content. + +## 6. Open questions + +- Whether CPython's own native-image behaviour holds with the fork's Compose in the same image (one + process, Skia and libpython both `dlopen`ed or linked); not tried. +- Where the native-image E2E runs in CI: GitHub's macOS runners have no GraalVM by default, and the + fork's static skiko must be built or cached. diff --git a/docs/investigations/gc-scheduling-investigation.md b/docs/investigations/gc-scheduling-investigation.md index 31b9b373a..5be003937 100644 --- a/docs/investigations/gc-scheduling-investigation.md +++ b/docs/investigations/gc-scheduling-investigation.md @@ -20,7 +20,7 @@ Conclusions (current): `ob_tid` instead of a refcount (header layout differs), and heap types are deferred-ref-counted, so the type's count is observable only after a collection. An eval-loop checkpoint provably does not fix it; two `PyGC_Collect()` calls in the test do. (§3) -- **The checkpoint fixes §1 on the GIL build; free-threaded it often reclaims nothing**, because +- **The checkpoint fixes §1 on the GIL build; free-threaded it often reclaims nothing**: because `_Py_RunGC` re-asks `gc_should_collect`, which gates generation 0 on growth of the **whole process's** memory footprint (the JVM's). Python-side allocation cannot move it. Not caused by per-call `PyGILState_Ensure/Release` scopes (tested, rejected). (§8, §8d) @@ -116,7 +116,7 @@ one `_Py_HandlePending` (BRC merge, QSBR sweep, scheduled collection if any). No `Python3.autoDrainInterval` outermost scopes, and only if `ReleaseCounter.released` has moved. `reachEvalCheckpointHoldingGIL` declines while `PyErr_Occurred()` is non-null. It is not taken from the cleaner thread (the queue belongs to the owning thread; running Python there is the ROADMAP §1 -deadlock) — pinned by `EvalCheckpointTest.testCleanerActivityAloneTakesNoCheckpoint`. +deadlock), pinned by `EvalCheckpointTest.testCleanerActivityAloneTakesNoCheckpoint`. `PyGC_Collect` is the one Stable ABI call that reclaims on behalf of other threads (it stops the world and merges each thread state), at heap-walk cost, and the only thing that materialises a deferred reference. @@ -128,7 +128,7 @@ The test asserts the *ordering*, not the numbers. | | ns/op | | |---|---:|---| -| `withGIL { }` — attach and detach | 170.51 | floor | +| `withGIL { }`, attach and detach | 170.51 | floor | | `Python3.drainPendingReleases()` | 302.70 | ~132 ns over floor | | `withGIL { Py_MakePendingCalls() }` | 197.34 | cheap, does not merge | | `Python3.exec("pass")` | 7 037.04 | 23× the checkpoint | @@ -211,14 +211,14 @@ Experiments (all in `FreeThreadedGCGateTest`; residue of 20,000 and collections | experiment | free-threaded | GIL | |---|---|---| -| `gc.get_count()[0]` shape across workload | sawtooth `[1728, 1488, 976, 464, 2464, 1488, 976, 464, 2464, 1488]`, 0 collections, residue 20,000 (memory gate declines) | — | -| threshold `(2000,10,10)` | 20,000 — 0 collections | 1,968 — 9 collections | -| `(2000,0,0)` (`old[0].threshold == 0` short-circuits `:2126`) | **1,318 — 8 collections** | 1,966 — 5 collections | -| `(2000,10,10)` again (A/B/A, one JVM) | 20,000 — 0 | 1,970 — 9 | +| `gc.get_count()[0]` shape across workload | sawtooth `[1728, 1488, 976, 464, 2464, 1488, 976, 464, 2464, 1488]`, 0 collections, residue 20,000 (memory gate declines) | - | +| threshold `(2000,10,10)` | 20,000, 0 collections | 1,968, 9 collections | +| `(2000,0,0)` (`old[0].threshold == 0` short-circuits `:2126`) | **1,318, 8 collections** | 1,966, 5 collections | +| `(2000,10,10)` again (A/B/A, one JVM) | 20,000, 0 | 1,970, 9 | | `threshold0` 2000 / 500 / 100 (bar 80,000 / 20,000 / 4,000) | 20,000 / 20,000 / **1,466 (3 collections)** | scales smoothly: 9 / 36 / 172 collections | -| baseline | 20,000 — 0 | 1,972 — 9 | -| 400 MB ballast faulted in midway | **9,754 — 1 collection** | 1,988 — 9 | -| ballast dropped | 20,000 — 0 | 1,968 — 9 | +| baseline | 20,000, 0 | 1,972, 9 | +| 400 MB ballast faulted in midway | **9,754, 1 collection** | 1,988, 9 | +| ballast dropped | 20,000, 0 | 1,968, 9 | The ballast result reproduced identically in three consecutive JVMs: JVM memory growth, and nothing else, collected Python cycles. Probe pitfall recorded: sampling `gc.get_count()` every 500 diff --git a/docs/investigations/jni-call-convention-audit.md b/docs/investigations/jni-call-convention-audit.md index 2c2e6bde6..5d50239d6 100644 --- a/docs/investigations/jni-call-convention-audit.md +++ b/docs/investigations/jni-call-convention-audit.md @@ -3,7 +3,7 @@ Android binds CPython through `RegisterNatives`. `@CriticalNative` and `@FastNative` both stop the ART collector for the duration of the call, and `@CriticalNative` receives no `JNIEnv`. So neither may be used for a function that can re-enter the runtime, which means any function that can run -Python (`__del__`, `__getattr__`, a module's top-level code) — with upcalls live, that Python can +Python (`__del__`, `__getattr__`, a module's top-level code), with upcalls live, that Python can be Kotlin. ## Conclusion @@ -45,13 +45,13 @@ All eight are leaves on the success path; all can allocate a GC-tracked exceptio path. That second column is conditional and second-order (the exception instance is GC-tracked, so it can cross the collection threshold, so the collector can run a `__del__`); it is not a reason to demote them, they are the measured hot path. It is why "provably a leaf" is not the promotion -test — the honest test is *leaf on the success path, and measured worth it*. +test, the honest test is *leaf on the success path, and measured worth it*. | call site | success path | failure path | |---|---|---| -| `Py_IsInitialized` | reads a global | — | -| `Py_GetVersion` | static `const char*` | — | -| `PyErr_Occurred` | reads thread state, borrowed | — | +| `Py_IsInitialized` | reads a global | - | +| `Py_GetVersion` | static `const char*` | - | +| `PyErr_Occurred` | reads thread state, borrowed | - | | `PyLong_FromLongLong` | non-GC allocation | preallocated `MemoryError` | | `PyUnicode_FromString` | non-GC allocation | `UnicodeDecodeError` | | `PyUnicode_AsUTF8` | caches UTF-8 form | `UnicodeEncodeError` on lone surrogates | @@ -62,7 +62,7 @@ test — the honest test is *leaf on the success path, and measured worth it*. Change in per-call cost, net of the Kotlin floor, from moving a function off ordinary JNI (source: `docs/design/downcall-design.md`, `androidMain/README.md`; ART emulators / device per those -documents — conditions are in them, not re-measured here): +documents, conditions are in them, not re-measured here): | API | ordinary → `@FastNative` (ns) | ordinary → `@CriticalNative` (ns) | |---|---|---| @@ -77,14 +77,14 @@ documents — conditions are in them, not re-measured here): `list → LongArray`, 1000 elements, at 50065.89 ns on API 36 hardware (50 ns/element) against a `@CriticalNative` net cost of 44.05 ns there (an upper bound on the convention's share, since the 50 ns also covers the loop and pointer conversion). Predicted after the twin: 44.05 → 3.55 ns per -element on API 36 — a prediction, not a recorded measurement. +element on API 36, a prediction, not a recorded measurement. ## The three rejected promotions that were not leaves | | audit said | actually | |---|---|---| | `PyGILState_Release` | "unlocks mutex (non-blocking)" | the release that drops the counter to zero deletes the thread state; clearing it decrefs its dict and exception state, so `__del__` runs | -| `PyThreadState_GetDict` | "reads struct pointer" | allocates the thread dict with `PyDict_New` on first call per thread — a GC-tracked allocation | +| `PyThreadState_GetDict` | "reads struct pointer" | allocates the thread dict with `PyDict_New` on first call per thread, a GC-tracked allocation | | `PyEval_InitThreads` | "initializes locking" | a no-op kept for the stable ABI; promoting it buys only the convention delta | The genuine leaves among the eight are `Py_IncRef`, `Py_NewRef`, `Py_XNewRef`, diff --git a/docs/investigations/typedpython-kernel-backend.md b/docs/investigations/typedpython-kernel-backend.md new file mode 100644 index 000000000..b3fc7f75d --- /dev/null +++ b/docs/investigations/typedpython-kernel-backend.md @@ -0,0 +1,206 @@ +# TypedPython 커널 백엔드 조사 (2026-10-04) + +표기: **[V]** = verified (출처 URL 병기, 이번 조사에서 직접 fetch/search 결과로 확인). **[I]** = unverified / my inference. +출처 중 WebSearch 요약은 검색 엔진 요약문이므로, 숫자나 버전은 채택 전에 원문 재확인이 필요하다. + +결론의 반영: `docs/design/typedpython.md` §5 (2026-10-04 결정). IR 선택은 후속 조사 [`typedpython-kernel-ir.md`](typedpython-kernel-ir.md) 가 다듬었다. + +## 0. 조사 당시의 계획 (읽은 문서, 이후 §5 가 바뀌었다) + +- typedpython.md §2.1: 유효한 파이썬만. 커널도 수정 없이 CPython 에서 실행돼 참조 구현이 된다. +- §5: iOS 는 런타임 코드 생성 금지이므로 AOT 가 하드 제약. 경로는 `@kernel` 서브셋 -> 자체 IR -> SPIR-V -> (Vulkan | SPIRV-Cross->MSL | naga->WGSL), PTX 별도. 착지점은 torchnative 의 `torch.library` 커스텀 op. SIMT 먼저, 타일은 나중. +- torchnative DESIGN.md: Triton 기각(JIT), 후보는 손으로 쓴 Metal/NEON, CubeCL("성숙도 확인 필요"), AOT Triton. 안드로이드 GPU 는 "Vulkan/wgpu 백엔드 도입 또는 작성" 으로 비어 있다. + +## 1. TileLang (tile-ai/tilelang) + +- 백엔드 [V github.com/tile-ai/tilelang]: CUDA(주), ROCm/HIP, Ascend, Apple Metal 은 supported. LLVM CPU, CuTe DSL, WebGPU 는 README 상 experimental. v0.1.13 (2026-08-03 출시라는 검색 요약), 문서 사이트는 0.1.14. +- 불일치 [V]: 공식 docs(tilelang.com/get_started/targets.html)는 Metal, LLVM, WebGPU, C 를 모두 "Fully supported" 로 적지만 README 는 WebGPU 를 experimental 로 적는다. 문서 쪽이 낙관적이라고 보는 게 안전하다 [I]. +- Vulkan/SPIR-V: 타깃 문서에 항목 없음. 로드맵 이슈 #56 (2025-01-26 개설) 에 "planned" 로만 있고 진행 표시 없음 [V github.com/tile-ai/tilelang/issues/56]. 즉 Android Vulkan 경로는 **없다** (Mali/Adreno 불가) [V 부재 확인, 완료 여부는 이슈 본문 한계로 100% 는 아님]. +- 라이선스: MIT [V raw.githubusercontent.com/tile-ai/tilelang/main/LICENSE]. 2024-12~2025-03 기간 Microsoft 협업 부가 조항 언급이 있다. TVM 은 Apache-2.0 [I, 기억 기반]. +- AOT: README 가 "AOT export" 를 기능으로 언급 [V]. 단 무엇이 export 되는지(소스 vs 바이너리, iOS 에 싣는 형태)는 확인 못함. 타깃 문서에 `C` 타깃(소스만 출력)이 있어 소스 생성은 가능 [V]. +- 순수 파이썬 CPU 실행: LLVM CPU 타깃이 있으나 "빌드 시 USE_LLVM=ON, LLVM 15+" 필요, 검색 요약상 experimental [V 검색요약]. 파이썬 인터프리터가 커널 본문을 그대로 돌리는 참조 실행(우리 §2.1 요건)은 TileLang 의 `T.prim_func` 가 TVM script 빌더라서 **그대로는 불가** [I: 커널이 파이썬 AST 를 직접 실행하지 않고 DSL 빌더로 구성된다는 구조적 추론, 실행해 보지 않음]. +- TVM 의 단점(빌드 복잡도, 바이너리 크기, 컴파일 시간, 포크 변경, free-threading 지원): **이번 조사에서 확인하지 못함**. README 가 TVM 을 핵심 의존성으로 명시한다는 것만 [V]. 나머지는 [I] 이며 측정이 필요하다 (§7). +- 우리 요건과 맞는 점: 타일 추상화, 텐서 코어/simdgroup 접근이 1급. 어긋나는 점: 새 DSL 문법(`T.` API), "유효한 파이썬만" 은 만족하나 CPython 에서 의미 있게 실행되지는 않음, iOS/Android Vulkan 부재. + +## 2. wgpu + naga + +### 2.1 현재 상태 +- wgpu 문서 v30.0.1 [V docs.rs/wgpu/latest/wgpu/struct.Features.html]: + - SUBGROUP: Vulkan, DX12, Metal 에서 지원, "WebGPU 표준으로 곧 이동 예상". + - SHADER_F16: Vulkan, Metal, DX12, WebGPU 모두. + - EXPERIMENTAL_COOPERATIVE_MATRIX: 문서상 8x8 f32 로 제한, **native 전용** (Metal, Vulkan). + - PASSTHROUGH_SHADERS: Vulkan, DX12, Metal, WebGPU 에서 셰이더를 파싱 없이 직접 제출 가능. 즉 우리가 SPIR-V/MSL 을 직접 만들어 wgpu 런타임에만 얹는 길이 열려 있다. + - 해당 문서에 PUSH_CONSTANTS, int8 dot 항목은 보이지 않음 (이 문서 요약 기준. 부재의 증거가 아님) [V 한계 있음]. +- 협력 행렬 PR #8251: 2025-12-22 merge, Vulkan(KHR_cooperative_matrix)과 Metal(simdgroup) 대상, 후속 PR 로 정수(i32/u32/i8/u8)와 비정방 타일 [V github.com/gfx-rs/wgpu/pull/8251, /pull/9490, /pull/10414]. 문서의 "8x8 f32 만" 과 후속 PR 이 어긋나므로 어느 릴리스에 어디까지 들어갔는지는 미확정. +- WebGPU 표준: subgroups 는 Chrome 134 에서 출시 [V developer.chrome.com/blog/new-in-webgpu-134], Chrome 144 에서 subgroup_id/num_subgroups 추가 [V 검색요약]. subgroup matrix 제안은 2026 에도 활발히 수정 중(2026-09 D3D12 반영 PR) 이므로 **표준 미확정, 브라우저 출시 전** [V github.com/gpuweb/gpuweb/issues/4195, PR #10964; 출시 여부는 확인 못함]. +- naga: subgroupElect 외 subgroup 연산 지원 [V github.com/gfx-rs/wgpu/issues/7396, 이슈 시점 기준]. + +### 2.2 성능 증거 +- Burn 블로그 요약: Vulkan(SPIR-V) 컴파일러가 WGSL 보다 matmul 에서 크게 낫다. 이유는 SPIR-V 쪽에서 텐서 코어와 f16 을 쓸 수 있기 때문 [V 검색요약, burn.dev/blog/sota-multiplatform-matmul]. 같은 맥락에서 "CUDA/Vulkan 이 WebGPU 보다 prefill 최대 10x, decode 2.5x 빠르고 Metal 이 WebGPU 보다 prefill 2x 이상" 이라는 문장이 나왔으나, 어느 모델/논문의 수치인지 출처 귀속이 불분명하다 (Llamas on the Web arXiv 2605.20706 로 추정되나 PDF 본문에서 수치를 직접 확인하지 못함) [V 검색요약, 귀속 미확인]. +- Meganeura (arXiv 2608.01563): wgpu 를 거치지 않고 Vulkan/Metal 로 직접 컴파일하며, wgpu 층의 오버헤드를 이유로 든다고 요약됨 [V 요약, 수치 미확인]. +- 결론 [I]: ML 커널에서 "wgpu 의 WGSL 경로" 는 협력 행렬/int8/f16 접근에서 native 에 뒤진다. wgpu 를 **런타임** 으로만 쓰고 셰이더는 PASSTHROUGH 로 넣는 것이 중간 길이다. + +### 2.3 모바일, torchnative 공존 +- Android Vulkan, iOS Metal 은 wgpu 가 지원하는 백엔드 [I: 일반 지식, 이번에 문서로 재확인하지 않음]. +- torchnative 의 `mps`/`vulkan` 은 torch 디바이스이고, wgpu 는 별도 디바이스/버퍼 소유권을 가진다. 같은 MTLBuffer/VkBuffer 를 공유하려면 interop 이 필요하다 [I]. 커스텀 op 가 자체 Vulkan/Metal 호출을 쓰는 편이 버퍼 공유가 쉽다 [I]. + +## 3. CubeCL (tracel-ai/cubecl) + +- 백엔드: CUDA, ROCm(HIP), wgpu 를 통한 Vulkan(SPIR-V)/WebGPU(WGSL)/Metal, CPU(SIMD). 라이선스 MIT/Apache-2.0. 상태 "Alpha, Burn 이 프로덕션 사용" [V github.com/tracel-ai/cubecl]. +- 네이티브 Metal: cubecl-metal 이 0.11.0-pre.4 에 추가되고 MSL 경로가 WGSL 폴백과 병존. 관련 이슈들(컴파일 실패, 폴백 혼선)이 최근 열려 있어 **이 경로는 아직 불안정** [V 검색요약, github.com/tracel-ai/cubecl/pull/1734, /issues/1722, /issues/1730]. +- 타일/매트릭스: cubek/cubecl 의 matmul 이 텐서 코어, double buffering, 벡터화로 cuBLAS/CUTLASS 와 경쟁(RTX 4080 에서 Simple 변형이 대체로 앞섬). Apple M2 Pro 에서는 Specialization 변형이 Metal 의 plane 제어 부족으로 뒤짐 [V burn.dev/blog/sota-multiplatform-matmul]. +- AOT: 공식 입장이 "JIT, 실제 실행되는 변형만 생성" [V github 요약]. 즉 **iOS 에서 그대로는 불가**. 다만 IR 에서 소스(WGSL/MSL/SPIR-V)를 빌드 타임에 뽑아 저장하는 길은 코드 구조상 가능해 보인다 [I, 확인 안 함]. +- 파이썬 프런트엔드 -> CubeCL IR: CubeCL 의 입구는 Rust `#[cube]` 매크로가 IR 빌더 호출로 바꾸는 것 [V]. IR 은 Rust 크레이트 내부 API 라 파이썬에서 직접 만들려면 Rust 쪽에 IR 을 직렬화하는 얇은 계층을 우리가 써야 한다 [I]. 또 comptime 특수화는 Rust 제네릭에 묶여 있어 파이썬 쪽 표현이 어렵다 [I]. +- CPU 참조 실행: CubeCL 자체 CPU 런타임이 있으나 이는 Rust 쪽이고 파이썬 인터프리터 실행이 아니다 [I]. + +## 4. MLIR 계열 (Triton 스택, IREE, Mojo) + +- Triton: JIT 이 전제. Apple 용 외부 백엔드(triton-metal, triton-msl, triton-ext PR #126/#127)가 존재하며 MSL 을 생성하고 upstream test_core 를 통과한다고 주장하나, 개인 프로젝트이며 fp8/mxfp 행렬 유닛 없음, 1024 스레드/32KB threadgroup 상한 등 하드웨어 한계가 있다 [V 검색요약, github.com/bledden/triton-msl]. Vulkan/SPIR-V 백엔드는 이번 검색에서 찾지 못함 [V 부재는 검색 한계]. 이전 조사(FlexLA, ICLR 2026)는 Triton 위 AOT 디스패처 선례 [V torchnative DESIGN.md 인용]. +- IREE: AOT 중심, Vulkan/Metal/CUDA/CPU, "Android, iOS, 베어메탈 지원", 런타임은 임베디드에서 약 30KB 까지 작아질 수 있음 [V 검색요약, iree.dev]. 그러나 이는 최소 구성이고 GPU 백엔드 포함 모바일 앱의 실제 크기와 컴파일러 빌드 부담은 확인 못함 [I]. 입력 단위가 모델(그래프) 이라, 단일 커널 DSL 을 임베드하려면 linalg/TOSA/StableHLO 로 내려야 한다 [I]. +- Mojo: Apple GPU 는 nightly 에서 초기 지원, 경로가 Mojo -> LLVM IR -> AIR 비트코드 -> metallib. MAX 그래프/모델은 아직 Apple GPU 에서 미지원(조사 시점 검색 요약) [V forum.modular.com/t/apple-silicon-gpu-support-in-mojo/2295]. Modular 25.6 이 NVIDIA/AMD/Apple 통합을 표방 [V modular.com 블로그 제목]. 모조의 SIMD 구현 세부(SIMD[dtype,width] 1급 타입 등)는 이번에 재확인하지 않음 [I, 일반 지식]. +- 이 계열의 공통 한계: 컴파일러 빌드/배포가 무겁고, 우리 "파이썬 AST -> IR" 프런트엔드를 MLIR 로 올리려면 MLIR 바인딩을 끌어와야 한다 [I]. + +## 5. 자체 소형 IR -> SPIR-V (현재 계획) + +만들어야 하는 것 [I, 다른 시스템 경험에 근거한 추정. 규모는 측정 아님]: +1. 프런트엔드: `@kernel` 파이썬 AST 서브셋 -> 타입된 IR. (등급 1 의 Pyrefly 게이트와 공유 가능) +2. SIMT 코드 생성: SPIR-V 를 직접 emit (rspirv 같은 크레이트나 직접 binary writer). 구조화된 제어 흐름 규칙, 메모리 모델, 용량 한정자 처리가 필요. +3. 공유 메모리, 배리어, 원자 연산, 서브그룹 연산. +4. 행렬 유닛: SPIR-V `OpCooperativeMatrix*KHR` 직접 emit (Vulkan 경로). Metal 은 SPIRV-Cross 가 simdgroup_matrix 로 내려주는지 불확실. SPIRV-Cross 의 협력 행렬 지원은 이번에 확인 못함 [I]. 한편 naga 는 협력 행렬 입력 WGSL, 출력 SPIR-V/MSL/WGSL 이 구현 중 [V wgpu PR #8251]. +5. 타일 추상화를 올리려면 레이아웃 추론, 소프트웨어 파이프라이닝(cp.async 대응), 레지스터/공유 메모리 swizzle, autotuning 대체물(AOT 이므로 빌드 타임 탐색)이 필요. 이것이 Triton/TileLang 이 수년 투자한 부분. +6. Metal 4 `matmul2d`/cooperative_tensor 는 SPIR-V 경로로 접근 불가 (MSL 4 전용, MPP 라이브러리) [V WWDC26 330, developer.apple.com]. Apple Neural Accelerator(M5) 를 쓰려면 SPIR-V -> MSL 변환이 아니라 MSL 직접 emit 이 필요하다 [I]. +7. Vulkan 쪽 협력 행렬 하드웨어: Adreno 는 VK_KHR_cooperative_matrix + VK_QCOM_cooperative_matrix_conversion 이 있는 신형에서만 llama.cpp 가 활성화 중, Mali 는 일부(PanVK) 외에는 없음으로 보고됨 [V llama.cpp PR #29328 검색요약]. 따라서 **Android 에서는 협력 행렬이 기본값이 아니라 선택 경로**이고 SIMT/서브그룹 폴백이 반드시 필요하다. + +## 6. CPU SIMD 쪽 + +- 우리 `@compiled` 는 자체 typed IR -> C (typedpython.md §4.3). 선택지 [I, 이번에 재조사 안 함]: + a. C 벡터 확장 (`__attribute__((vector_size))`, clang/GCC 공통, MSVC 불가). 소스 생성이 단순하고 NEON/AVX 로 내려감. 폭이 고정이라 SVE 류는 못 씀. + b. Highway: 런타임 디스패치와 가변 폭 지원. C++ 템플릿이라 C 생성기와 결합하려면 C++ 로 emit. 라이선스 Apache-2.0/BSD-3 [I, 기억 기반]. + c. LLVM 직접 emit: 가장 강력하나 의존성 최대. iOS 앱에 LLVM 을 싣는 것은 비현실적이고 AOT 빌드 머신에서만 쓸 수 있음. 빌드 타임 도구로서는 가능. + d. 모조 방식: `SIMD[dtype,N]` 을 1급 타입으로 두고 LLVM 에 내림 [I 일반 지식]. +- 권고(추정): a 를 기본, 핵심 BLAS 류는 손으로 쓴 NEON (torchnative 가 이미 후보로 둠), 성능이 필요한 곳에만 b. + +## 7. 비교표 + +범례: O 가능(검증), o 가능 추정, X 불가, ? 미확인. + +| 항목 | TileLang | wgpu+naga | CubeCL | MLIR(IREE/Triton/Mojo) | 자체 IR->SPIR-V | +|---|---|---|---|---|---| +| iOS AOT | ? (export 있으나 iOS 형태 미확인) | o (PASSTHROUGH 로 MSL/SPIR-V 제출 가능; 런타임은 Rust 크레이트) | X 기본(JIT). 소스 추출 개조 필요 [I] | IREE o (AOT 전제). Triton X (JIT). Mojo ? | O 설계 목표 | +| Android Vulkan | X (SPIR-V 미구현) | O (wgpu Vulkan 백엔드) | O (SPIR-V 컴파일러, JIT) | IREE O, 그 외 ? | o (직접 emit) | +| Metal | O 소스 생성(macOS, iOS ?) | O (naga MSL) | O 네이티브 cubecl-metal 신규/불안정 | IREE o, Triton 외부 포크, Mojo nightly | o (SPIRV-Cross 또는 MSL 직접) | +| WebGPU | 실험 | O (WGSL) | O (WGSL, SPIR-V 대비 느림) | IREE ? | o (naga) | +| CUDA | O (주력) | X (wgpu 에 CUDA 없음) | O | Triton/Mojo O | PTX 별도 작업 | +| 행렬 유닛 | O (CUDA/HIP/Metal) | 실험(협력 행렬, native 전용, WebGPU 표준 미확정) | O (SPIR-V/CUDA 경로, WGSL 경로 약함) | O (Triton/Mojo) | 직접 구현 필요 | +| 타일 추상화 | O | X | 부분(cubek 라이브러리 수준) | Triton O | X (뒤로 미룸) | +| 순수 파이썬 CPU 참조 실행 | X [I] | 해당 없음 | 해당 없음 | Triton 은 interpreter 모드 있음 [I 기억] | O (§2.1 설계) | +| 의존성 무게 | 큼(TVM) [I] | 중(Rust) | 중(Rust) | 큼 | 작음(우리 코드) | +| 라이선스 | MIT (TVM Apache-2.0 [I]) | MIT/Apache-2.0 [I] | MIT/Apache-2.0 [V] | IREE Apache-2.0 [I], Mojo 폐쇄적 부분 있음 [I] | 해당 없음 | +| 성숙도 | 활발, 릴리스 0.1.x | 활발, 협력 행렬 신기능 | Alpha, 변화 빠름 | IREE 성숙, Mojo Apple 초기 | 0 | + +## 8. 각 선택지가 **못 하는** 것 + +- TileLang: Android Vulkan 불가, 파이썬 인터프리터 참조 실행 불가(추정), TVM 빌드 체인을 우리 Gradle/uv 배포에 넣는 부담, iOS AOT 형태 미확인. +- wgpu/naga: WebGPU 표준에 협력 행렬 아직 없음, WGSL 경로는 int8 dot / 협력 행렬 / 큰 공유 메모리에서 native 에 뒤짐(Burn 사례), Metal 4 tensor ops 접근 불가, CUDA 불가. 런타임이 Rust 라 torchnative 의 버퍼와 별도 소유. +- CubeCL: JIT 전제로 iOS 불가(개조 전), Rust 가 소스 언어라 파이썬 프런트엔드 입구가 없음, Alpha, Metal 네이티브 경로 불안정. +- MLIR: 무거운 빌드, 커널 단위 DSL 아님(IREE), Triton 은 JIT, Mojo 는 우리가 소유할 수 없는 폐쇄 도구. +- 자체 IR: 타일 수준 최적화(파이프라인, 레이아웃)를 전부 직접 만들어야 하고, 벤더 드라이버 버그와 기기별 튜닝은 어떤 선택지도 대신해 주지 않는다. Apple Neural Accelerator 는 SPIR-V 를 거치면 못 쓴다. + +## 9. 권고 (불확실성 명시) + +1. **프런트엔드와 IR 은 자체 소유를 유지한다.** 이유: §2.1(참조 CPU 실행 = 파이썬 그대로)을 만족하는 후보는 이것뿐이고 [I], 등급 1 의 게이트와 공유된다. 외부 시스템은 모두 자체 DSL 이 입구라서 §2.1 과 충돌한다 [V 각 DSL 의 입구 확인, 충돌 판단은 I]. +2. **백엔드는 한 개로 고정하지 말고 emitter 를 교체 가능한 계층으로 둔다.** 1단계는 SIMT 로 SPIR-V(Vulkan, Android)와 MSL(Apple) 두 개를 직접 emit. SPIRV-Cross 경유보다 MSL 직접 emit 을 권한다(Metal 4 tensor ops, simdgroup_matrix 에 닿으려면 필요) [I]. +3. **런타임은 먼저 wgpu 를 쓰지 않는다.** torchnative 의 mps/vulkan 디바이스와 버퍼를 공유해야 하는 커스텀 op 라는 위치 때문에, 자체 얇은 Vulkan/Metal 디스패처가 맞을 가능성이 높다 [I]. 웹 타깃에서만 wgpu/WebGPU 를 쓰고, 이때 WGSL 의 성능 한계를 감수한다. 협력 행렬이 필요한 웹 경로는 표준 확정 이후. +4. **CubeCL 은 "라이브러리 수준 참조" 로 쓰고 의존하지 않는다.** cubek matmul 의 타일 알고리즘(double buffering, line size)과 벤치 결과는 우리 커널 설계의 비교 기준으로 가치가 크다 [V]. 의존하려면 AOT 개조와 파이썬 IR 직렬화가 먼저다 [I]. +5. **TileLang 은 데스크톱 CUDA/Metal(macOS) 에서 비교 기준(baseline)으로 쓴다.** 모바일 경로가 없으므로 주 백엔드로는 부적합 [V Vulkan 부재 / I 판단]. +6. 타일 추상화는 SIMT 가 돌아간 뒤. 처음부터 Triton 급을 목표로 하면 §5 의 항목 5 가 프로젝트를 잡아먹는다 [I]. +7. CPU SIMD 는 C 벡터 확장으로 시작 [I]. + +불확실성: (a) TVM/TileLang 의 실제 비용을 측정하지 않았음, (b) wgpu 협력 행렬의 릴리스별 현황과 성능, (c) Adreno/Mali 협력 행렬 보급률, (d) 직접 emit 한 SPIR-V 가 기기별 드라이버에서 얼마나 안 깨지는지, (e) SPIR-V -> MSL 에서 협력 행렬이 내려가는지(SPIRV-Cross 미확인). + +## 10. 결정을 위해 필요한 측정 + +1. TileLang 을 `pip`/`uv` 로 설치한 크기, 소스 빌드 시간, Python 3.13/3.14t 지원 여부, 소스 export 로 얻는 MSL/CUDA 가 iOS 앱에 링크 가능한지(실제로 export 후 Xcode 에서 컴파일). +2. 동일 커널(GEMM 1024^3 f16, softmax, 융합 어텐션) 을 (i) 손 작성 MSL (ii) 우리 SPIR-V->SPIRV-Cross MSL (iii) naga MSL (iv) CubeCL Metal 로 M-시리즈/A-시리즈에서 GFLOPS 비교. Apple 은 simdgroup_matrix 와 Metal 4 matmul2d 둘 다. +3. Android: Adreno 7xx/8xx, Mali G7xx 에서 VK_KHR_cooperative_matrix 노출 여부와 SIMT 폴백 대비 속도(실기 3종 이상). +4. wgpu PASSTHROUGH + 우리 SPIR-V 와 직접 Vulkan 디스패치의 호출당 오버헤드(소형 커널 반복 10만 회), 그리고 torchnative 텐서 버퍼 공유 가능 여부. +5. 직접 emit SPIR-V 의 spirv-val, 드라이버 크래시율(가능한 많은 기기/Android 버전, CI 팜). +6. CPU: C 벡터 확장 vs 손 NEON vs Highway 로 dot/GEMV/q4 matmul 비교(torchnative 의 기존 커널 기준). 측정은 규칙 8 에 따라 단독, 비필터로. +7. 자체 IR 구현 비용 추정을 위한 프로토타입: SIMT vector-add + reduction + 공유 메모리 GEMM 을 SPIR-V 와 MSL 로 emit 하는 데 걸리는 기간. + +## 11. 이 조사 방법이 찾지 못하는 것 + +웹 검색 요약과 문서 페이지 요약(작은 모델이 변환한 결과)에 의존했으므로, 실제 성능 수치, 실기 드라이버 버그, 빌드해 보아야 보이는 의존성 문제, 요약기가 누락하거나 잘못 적은 세부(특히 PDF 본문 수치)는 찾지 못한다. + +## 12. torchnative 를 커널 컴파일러의 일부로 보기 + +표기는 위와 같다. 이 절의 **[R]** = verified (torchnative 저장소의 파일, 이번에 직접 읽음. 경로 병기, 실행하지는 않았으므로 문서가 주장하는 측정값을 재현한 것은 아님). **[V]** 는 웹 출처, **[I]** 는 추론. + +### 12.0 읽고 나서 바뀌는 사실 (§2, §5 의 전제 수정) + +- torchnative 에는 **이미 자체 Vulkan 경로가 있다.** `torchnative/rust/torch_c/src/vulkan.rs` (3801줄), `ash`(loaded, dlopen) 기반, GLSL `.comp` 를 빌드 머신에서 `glslc` 로 컴파일해 `.spv` 를 **저장소에 체크인**하고 `include_bytes!` 로 싣는다. `torch_c/shaders/` 에 add, matmul, bmm, softmax, layer_norm, gelu, embedding, gather 등 f32 셰이더가 있다 [R: torch_c/shaders/compile.sh 와 목록]. +- 셰이더는 shape 마다 재생성하지 않는다. 모양은 push constant 로 넘기고 셰이더 하나가 모든 크기를 처리한다 [R: docs/devices/VULKAN.md §3]. +- `ash` vs `wgpu` 를 **기기(에뮬레이터)에서 둘 다 돌려 비교**했다. 둘 다 통과, ash 는 크레이트 4개 719 KB / 클린 빌드 8.5 s, wgpu 는 60 크레이트 6.77 MB / 24 s. 권고는 ash. 단 "Apple GPU 를 어떻게 채우나" 가 걸려 있어, candle 의 metal 을 켜면 ash, 커널을 직접 소유하면 wgpu 가 맞다고 명시하고 결정은 조율 세션에 남겼다 [R: docs/devices/VULKAN.md §4.1, §5.1-5.3]. 이 조사의 질문(커널을 우리가 소유)은 바로 그 두 번째 칸이다. +- 단 wgpu 측정은 `Limits::downlevel_defaults()` 로 돌렸고 이는 기기 한계가 아니라 wgpu 하한, 버퍼는 최대 4 MB 였다 [R: VULKAN.md 약 381행]. 큰 텐서·협력 행렬·f16 에서의 비교는 아직 없다. +- Apple 쪽은 `mps` 디바이스가 candle Metal 위에 있고, candle Metal 에는 조용한 CPU 폴백이 없으며 이 크레이트의 커널 중 54개 op 이 호스트 되읽기라 `NotImplementedError` 로 거절한다 [R: docs/devices/MPS.md]. `Cargo.toml` 은 Apple 에서 `accelerate` 만 켜고 `metal` 은 기본값으로 끈다는 주석이 있다 [R: torch_c/Cargo.toml, DESIGN.md 1150-1160]. +- 즉 §5 의 "SPIRV-Cross->MSL, naga->WGSL" 같은 **번역 계층 전략**은 torchnative 입장에서는 이미 한 번 검토되어 "가장 가벼운 것 = 직접 ash + 체크인된 SPIR-V" 로 기운 상태다. 그 방향과 정합하도록 §9 권고 3("wgpu 를 먼저 쓰지 않는다")은 근거가 강화된다. + +### 12.1 (a) 빌드 타임 그래프 캡처 -> 융합 구간 -> 커널 컴파일러 + +**상태 [R]**: `torch_c/src/capture.rs` + `docs/graph/CAPTURE.md`. `_aten_dispatch` 단일 문(door) 끝 한 줄에서 op 을 기록한다. 기록 = (op 이름 `aten..`, 피연산자 shape/dtype/device, 결과), 가드 = 입력의 **정확한** shape/dtype/device, 재생 = eager 와 대조. FX/ExportedProgram 과 1:1 대응이라 ExecuTorch 로 갈 수 있는 모양이라는 판단이 문서에 있다 (단 "부족한 네 가지" 가 명시됨, 이번에는 열람하지 않음). +**거절하는 것 [R: CAPTURE.md §4]**: 텐서 값 기반 제어 흐름(`_local_scalar_dense`), 인플레이스/별칭, **동적 shape**, 난수, 비텐서 결과, 컨테이너. 상수(가중치)는 참조로 잡아 가드하지 않음. 배치 차원은 자유롭지 않음. +**torch.compile 은 불가 [R]**: `docs/graph/COMPILE.md` 는 abi3 에서 PEP 523 프레임 평가 훅이 막힌다고 진단하고, 대안으로 `torch.export` 경로를 본다. `docs/graph/EXPORT6.md` 는 실제 transformers 40개 아키텍처 중 export 성공이 **0/40** (벽 8개 중 상당수 해결, 그래도 헤드라인은 0/40) 이라고 적는다. + +이 역할이 필요로 하는 것과 막는 것: + +1. **융합 패턴 선정기.** 기록에서 직선 구간을 찾아 "gated MLP", "RMSNorm+residual", "SDPA", "dequant+matmul" 같은 패턴을 추출. `NPUFUSE.md` 가 Intel NPU 에서 gated MLP 를 OpenVINO 그래프 하나로 낮춘 선례다 (단 NPU 에서는 아직 실행 안 됨, 문서가 스스로 명시) [R]. 이것은 새 컴파일러가 아니라 **패턴 매처**로 시작하는 게 현실적이다 [I]. +2. **동적 shape (LLM 디코드).** 막힘의 핵심. 디코드는 KV 길이가 매 스텝 변하고, 현재 캡처 가드는 정확한 shape 를 요구한다 [R]. 외부 사례: ExecuTorch 는 LLM 에서 sequence 차원을 dynamic 으로 표시하며, 정적 shape 만 되는 백엔드(예: QNN)에는 prefill(최대 길이로 패딩)과 단일 토큰 decode 모델을 따로 export 한다 [V docs.pytorch.org/executorch/1.0/llm/export-custom-llm.html, 검색요약]. 즉 업계도 "shape 별 그래프 2~몇 개 + 패딩" 으로 해결한다. torchnative 도 같은 길(prefill 그래프, decode 그래프를 각각 캡처, KV 는 최대 길이 사전 할당 + 길이는 스칼라 입력)을 택하는 것이 가능해 보인다 [I]. AOTInductor 가 동적 shape 를 어떻게 다루는지는 이번에 원문으로 확인하지 못했다 (ExecuTorch 의 Metal/CUDA 가 AOTInductor 를 활용 중이라는 검색요약만 [V]). +3. **커널 쪽 계약.** 융합 구간을 TypedPython `@kernel` 로 쓴 커널에 매핑하려면, 커널이 **shape 를 데이터(push constant/uniform)로 받는** 형태여야 한다. torchnative Vulkan 셰이더가 이미 그렇게 한다 [R]. TypedPython 커널 시그니처에 "shape/stride 를 런타임 인자로, tile 크기는 컴파일 타임 상수" 를 요구사항으로 넣는 것이 자연스럽다 [I]. +4. **자동 코드 생성은 이 단계에서 불필요.** 융합 영역을 즉석에서 Inductor 식으로 생성하려면 pointwise/reduction 스케줄러가 필요한데 규모가 크다 [I]. 1단계는 "사람이 `@kernel` 로 쓴 융합 커널 + 캡처 기록에서 패턴 매칭으로 치환" 이면 된다. 이것은 DESIGN.md §8 의 `use_kernel_forward_from_hub` 레이어 치환과 같은 자리다 [R]. + +못 하는 것: torch.compile 의 자동 융합 대체, 데이터 의존 제어 흐름(MoE 라우팅, 조기 종료), 인플레이스 KV 갱신(변이는 거절됨, 단 KV 캐시 갱신이 정확히 그 경로 [R: DESIGN.md §4 "주의"]). **KV 캐시 인플레이스가 캡처 거절과 정면 충돌**한다 점이 가장 큰 구체적 장애다 [I: 두 문서의 결합에서 도출]. + +### 12.2 (b) torchnative 디바이스 층을 커널 런타임으로 공유 + +**상태 [R]**: Vulkan(ash) 디바이스 존재(f32 위주 셰이더, 1훈련 스텝까지 도는 단계 VULKAN10), Metal 은 candle 경유 mps, CPU 는 candle + 자체 커널. 각 디바이스에서 Metal/Vulkan 의 저장소(storage)는 torchnative 가 소유한다. + +장점 [I]: TypedPython 커널이 별도 런타임을 갖지 않고 같은 버퍼·큐·동기화를 쓰면 (1) 호스트 복사 없음, (2) 커널 소유 문제(수명, 풀링)가 한 곳, (3) 앞서 §0 이 제기한 "Taichi 식 고립" 을 피한다. +필요한 것: +- torchnative `vulkan.rs` 에 **외부 SPIR-V 를 받는 "임의 커널 디스패치" 진입점**: 파이프라인 캐시, 디스크립터 레이아웃 선언(버퍼 개수, push constant 크기), 워크그룹 크기. 현재 프로브는 디스패치마다 파이프라인·풀을 만들고 부수며 이는 "프로브라서 그럼" 이라고 문서가 명시, 커널이 몇 개를 넘으면 얇은 내부 계층이 필요하다고 스스로 적었다 [R: VULKAN.md §5.2]. +- Metal 쪽: 현재 candle-metal 이 MTLDevice/큐를 소유한다. 커스텀 MSL 을 끼우려면 candle 의 command buffer 에 인코딩하거나 같은 MTLBuffer 를 받아 별도 인코딩해야 한다. candle-metal-kernels 가 `rust/vendor/` 에 벤더링되어 있어 [R] 수정은 가능하나 버퍼 공유 방식은 코드를 읽지 않아 모른다 [I]. +- 행렬 유닛 접근: 현재 셰이더에 cooperative matrix 사용 흔적은 확인하지 못함(.comp 이름만 열람, 내용 미열람) [R 한계]. +막힘: Vulkan 셰이더가 f32 뿐이면 AI 커널 목적(f16/bf16/int4 matmul)에 부족. 이는 TypedPython 커널의 첫 번째 실제 수요처다 [I]. + +**wgpu 선택지와의 상호작용 [R + I]**: torchnative 는 wgpu 를 이미 기기에서 통과시켜 둔 프로브(`vulkan_probe` 의 `--features wgpu-route`)를 보존하고 있다 [R]. 그래서 두 런타임이 공존하면 버퍼가 둘로 갈라진다. 권고: 네이티브(Android Vulkan, Apple Metal)는 torchnative 디바이스 층(ash/candle-metal), **웹 타깃만 wgpu(WebGPU)** 로 분리하고, 두 경로는 같은 IR 에서 SPIR-V/MSL/WGSL 을 각각 emit 하는 emitter 로만 만난다. 웹에서 torchnative 를 쓰는지는 `wasm_probe` 가 있다는 것만 확인 [R]. + +### 12.3 (c) torchnative CPU 경로를 수치 오라클로 + +**상태 [R]**: `torch_c/src/flash.rs` 는 ATen CPU flash-attention 을 bf16/fp16 에서 **bit-identical** (226136개 원소 중 차이 0) 로 재현한 "참조 구현" 이고, f32/f64 는 upstream 이 BLAS 를 쓰므로 비트 일치 불가(차이 4.17e-07 수준)라고 문서가 구분한다. `vulkan_probe/src/check.rs` 는 GPU 와 CPU 참조를 같은 하니스로 비교하는 패턴이다. +역할 적합성: +- 수치 검증의 좋은 오라클이 된다. 단 **무엇과 일치해야 하는가를 먼저 정해야 한다**: TypedPython 의 순수 파이썬 참조 실행(§2.1) vs torchnative CPU(candle) vs 상류 PyTorch. 세 개의 오라클이 서로 다른 reduction 순서를 쓰므로 f32 에서는 허용오차 기반(ULP 한도 선언) 비교가 필요하다 [I, flash.rs 의 문서가 이 현상을 직접 보여줌 R]. +- 기기에서 CPU 오라클을 쓰는 것(디바이스 내 자기 대조)은 이미 프로브가 한다. 같은 방식으로 `@kernel` 하나당 (입력 시드, CPU 참조, GPU 결과) 를 출력하는 테스트 형식을 재사용할 수 있다 [I]. +- 한계: 오라클이 candle 이라면 candle 의 의미론 차이(복사 지향, view/in-place 임피던스 [R]) 가 섞인다. 커널 단위 테스트는 raw 버퍼 입력이라 영향이 작다 [I]. + +### 12.4 (d) kernels 번들 리졸버를 착지 슬롯으로 + +**상태 [R: DESIGN.md §8]**: HF `kernels` 계약(백엔드 변형 선언, transformers 무수정 교체)을 채택하고, 해석 시점을 빌드 타임으로 옮겨 Gradle 플러그인이 변형을 골라 AOT 컴파일·번들, 런타임 리졸버가 번들을 조회. 타깃 표에서 Android GPU 는 `kernels` 에 `vulkan` 백엔드가 없어 **구멍**이고, 권고는 "1) Android 는 CPU 만으로 시작 + 2) 상류에 vulkan 백엔드 제안 병행". +TypedPython 입장 [I]: +- `@kernel` 하나가 `metal` / `vulkan`(비표준) / `cpu` / `cuda` 변형 번들을 산출하는 **`kernel-builder` 대체 빌더**가 되는 구도. 이것이 §5.4 의 "`torch.library` 커스텀 op 등록" 보다 구체적인 착지 형태다. 두 개를 동시에 만족시키려면: op 등록은 torchnative 의 ATen 디스패처(`aten..`), 변형 선택은 리졸버로 하는 이중 구조가 필요할 수 있다. 어느 쪽이 정본인지는 결정 필요. +- 업스트림 `vulkan` 백엔드 슬롯 제안과 TypedPython 의 Vulkan emitter 는 한 묶음으로 움직이는 게 맞다. 이것이 DESIGN.md §8 의 선택지 2 를 실체화한다. +- 막힘: 빌드 타임 AOT 컴파일을 Gradle 플러그인이 호출해야 하므로 TypedPython 컴파일러(파이썬 프로세스) 호출 비용과 증분 컴파일(SPEC N-10)의 연계가 필요 [I]. + +### 12.5 종합 (이 절의 권고, 모두 [I]) + +1. 가장 얇은 수직 슬라이스: **`@kernel` 1개 (f16 GEMV 또는 RMSNorm) -> SPIR-V 직접 emit -> torchnative `vulkan.rs` 에 임의 SPIR-V 디스패치 진입점 추가 -> 기기에서 CPU 오라클과 대조.** 이미 있는 프로브·체크인 SPIR-V 흐름과 같은 모양이라 새 위험이 가장 적다. +2. wgpu 는 **웹 전용**으로 한정. torchnative 의 기존 측정(719 KB vs 6.77 MB)이 모바일 네이티브에서의 기각 근거다 [R]. +3. 그래프 캡처(a)는 커널 컴파일러의 선행 조건이 아니다. 먼저 레이어 치환(`use_kernel_forward_from_hub` 식)으로 가고, 캡처 기반 융합은 KV 인플레이스와 동적 shape 문제가 풀린 뒤로 미룬다. torch.export 0/40 이 그 이유다 [R]. +4. 결정이 필요한 것: (i) Apple 은 candle-metal 에 끼울지, TypedPython 이 MSL 을 emit 하고 자체 Metal 디스패처를 둘지, (ii) 정본 등록 경로(ATen 디스패처 vs kernels 리졸버), (iii) Vulkan f16/cooperative matrix 를 torchnative 디바이스 층의 범위로 볼지. + +### 12.6 추가 측정 항목 (§10 에 더해) + +8. torchnative Vulkan: 디스패치 1회당 오버헤드 (파이프라인 캐시 전/후), 동일 matmul 의 f32 GFLOPS (Adreno/Mali 실기), f16 셰이더 가능 여부. +9. candle-metal 에 외부 MSL 커널을 같은 command buffer 로 끼울 수 있는지(코드 열람 + 프로토타입). +10. 캡처: Llama 급 1개 모델의 decode 스텝을 "KV 최대 길이 고정 + 길이 스칼라" 로 쓰면 캡처 가드를 통과하는지 (인플레이스 KV 갱신 때문에 현재는 거절될 것으로 예상, 미측정). +11. 세 오라클 (파이썬 참조, candle CPU, 상류 PyTorch) 의 f32 softmax/matmul 편차 크기. + +이 절의 조사 방법이 찾지 못하는 것: 문서의 측정값을 재현하지 않았고(문서가 주장하는 숫자를 그대로 인용), `vulkan.rs` 3801줄과 candle-metal 연동 코드는 본문을 읽지 않았으며, 문서가 "해결됨" 이라 적은 항목이 현재 코드에서도 참인지는 확인하지 못한다. diff --git a/docs/investigations/typedpython-kernel-ir.md b/docs/investigations/typedpython-kernel-ir.md new file mode 100644 index 000000000..76d438456 --- /dev/null +++ b/docs/investigations/typedpython-kernel-ir.md @@ -0,0 +1,128 @@ +# TypedPython 커널 컴파일러 IR 선택 (2026-10-04) + +표기: **[V]** = verified (직접 실행한 명령 또는 fetch 한 URL), **[R]** = 로컬 저장소 파일을 직접 읽음, **[I]** = inference. +결론의 반영: `docs/design/typedpython.md` §5.4 (2026-10-04 결정). 조사 당시 작업 위치는 git 에 넣지 않은 `.tmp/research/` 였다(스크립트와 venv 는 커밋하지 않았다). venv 두 개(`venv` = py3.13 + xdsl, `venv314t` = py3.14 free-threaded + MLIR 바인딩 wheel). 스크립트 `t_spirv.py`, `t_bin.py`, `t_ptx.py`. + +## 0. 전제 (읽은 문서) + +- [R] typedpython.md §2.1: 유효한 파이썬만, 커널도 CPython 에서 그대로 실행(참조 구현). §5.1: iOS 때문에 AOT 가 하드 제약. §5.3 현재 경로: 자체 소형 IR -> SPIR-V -> (Vulkan | SPIRV-Cross->MSL | naga->WGSL), PTX 별도. 착지점 torchnative. +- [R] torchnative `vulkan.rs` (3801줄): `ash` 로 로더를 dlopen, GLSL 을 `glslc` 로 컴파일한 `.spv` 를 저장소에 체크인하고 `include_bytes!` 로 싣는다(71행). `create_shader_module`(692행) -> `create_compute_pipelines`(750행), 형상은 push constant(866행) 으로 전달, 디스패치는 `dispatch_kernel` / `dispatch_kernel_words`(788, 801행). 즉 **임의 SPIR-V 워드열을 받는 진입점이 이미 `dispatch_kernel_words` 모양으로 존재**하고, 첫 슬라이스의 도착 지점은 "`.spv` 를 컴파일러가 만든 것으로 바꾸는 것" 이다 (이전 조사 §12 의 "임의 SPIR-V 진입점이 필요" 는 이 파일의 현재 상태로 보면 부분적으로 이미 해결됐을 가능성, 단 파이프라인 캐시 상태는 읽지 않음) [R + I]. +- 이전 조사(kernel-backend.md)의 "자체 프런트엔드 + 자체 IR" 권고는 MLIR 을 내부 인프라로 평가하지 않았다. 이 문서가 그 공백을 채운다. + +프런트엔드(valid Python `@kernel` 서브셋 파싱, 등급 1 게이트와 공유)는 어느 선택지에서도 우리 몫이다. 선택지가 갈리는 곳은 **프런트엔드 뒤: IR, 변환(tiling/layout/pipelining), 코드 방출(emitter)** 이다. + +## 1. 직접 확인한 사실 (이번 조사에서 실행한 것) + +### 1.1 MLIR Python 바인딩 wheel + +- [V] PyPI 의 `mlir-python-bindings` 는 **이름 선점용 빈 placeholder** (0.0.1, "not the real distribution"). `mlir` 도 2023 년 빈 패키지. 즉 `uv add mlir-python-bindings` 는 진짜를 주지 않는다 (`curl pypi.org/pypi//json`). +- [V] 실제 wheel 은 GitHub release `llvm/eudsl` 태그 `mlir-python-bindings` 에 있다 (GitHub API 로 목록 조회: 658 assets, 최신 `mlir_python_bindings-20261004+758aa4a92-*`, 거의 매일 갱신). 설치 가이드: `pip install mlir-python-bindings -f https://llvm.github.io/eudsl` (eudsl README 요약). 이전 저장소 `makslevental/mlir-wheels` 는 2025-11-29 archive, eudsl 로 이전됨 [V WebFetch]. +- [V] 플랫폼/파이썬 매트릭스 (asset 이름 집계): cp310, 311, 312, 313, 314, **cp314t(free-threaded)** 각각 macOS arm64(`macosx_13_0_arm64`), Linux x86_64/aarch64(manylinux_2_34), Windows amd64. **cp315 / cp315t wheel 은 없음** (`cp315` 문자열 asset 0개). 크기 wheel 당 58 ~ 76 MB, 설치 후 `mlir/` 디렉터리 253 MB (macOS cp314t). +- [V] 실제로 설치해 실행: `uv venv venv314t --python 3.14t; uv pip install --python venv314t/bin/python ` 성공. `sys.version` = "3.14.7 free-threading build", `import mlir` 후 `sys._is_gil_enabled()` = **False** (GIL 이 다시 켜지지 않음). 등록된 dialect: spirv, gpu, nvvm, vector, linalg, transform, amdgpu, x86, nvgpu, emitc, llvm, scf, memref, tensor, tosa 등 (`pkgutil.iter_modules(mlir.dialects.__path__)`). +- [V] 공식 문서(mlir.llvm.org/docs/Bindings/Python/): free-threading 은 3.13+ 에서 "독립된 Context 라면 멀티스레드 안전". 안전하지 않다고 명시한 것: IR printing 과 PassManager 동시 사용, location error emit, module dump, transform interpreter, **GPU dialect 관련 작업**. 우리는 컴파일을 빌드 타임에 단일 프로세스/스레드로 돌리면 되므로 영향이 작다 [I]. +- [V] 동작 증명 1, **SPIR-V 바이너리**: `gpu.module @k [#spirv.target_env<...>] { spirv.module Logical GLSL450 requires #spirv.vce {...} }` 에 `gpu-module-to-binary` 를 돌리면 `gpu.binary` 에 `\03\02#\07` (SPIR-V magic 0x07230203) 로 시작하는 바이너리 blob 이 나온다 (`t_bin.py`). 즉 **파이썬 안에서 SPIR-V 직렬화까지 도달**. 단 `vce_triple` 없으면 "module must have 'vce_triple' attribute to be serializeable" 로 실패. +- [V] 동작 증명 2, **gpu -> spirv 하향**: `gpu.func` (global_id, memref.load, arith.addf, memref.store) 에 `convert-gpu-to-spirv` 를 돌리면 `spirv.func` 가 나온다 (`t_spirv.py`; StorageBuffer 구조체 래핑, GlobalInvocationId builtin 포함). 이어 `gpu-module-to-binary` 를 같은 모듈에 바로 돌리면 "the module has no target attributes" 로 실패: 하향 결과의 `spirv.module` 이 `gpu.module` 의 형제로 남고 target 속성이 없기 때문. **하향 산출물을 `gpu.module [target]` 안에 넣는 접합 코드는 우리가 써야 한다** (실행해 보지 않음, [I] 난이도 낮음). 이 end-to-end(f32 add -> .spv 바이트) 는 **검증하지 못했다**. +- [V] 동작 증명 3, **PTX**: `gpu.module @k [#nvvm.target]` 에 `gpu.module(convert-gpu-to-nvvm,canonicalize),gpu-module-to-binary{format=isa}` 를 돌리면 `.version 7.0 .target sm_80 ... .visible .entry f(` 로 시작하는 PTX 텍스트가 나온다 (`t_ptx.py`). wheel 에 NVPTX 백엔드가 들어 있다는 뜻. +- [V] `spirv.KHR.CooperativeMatrixLoad/Store/MulAdd/Length` op 이 파이썬 dialect 모듈(`mlir.dialects._spirv_ops_gen`)에 있음. `gpu-lower-to-nvvm-pipeline`, `convert-vector-to-spirv`, `convert-arith-to-spirv`, `convert-nvgpu-to-nvvm` 패스가 파싱됨. +- [V] 확인 못 한 패스 이름: `spirv-lower-abi-attrs`, `spirv-update-vce`, `convert-gpu-to-nvvm` (Context 없이 파싱 시도했거나 이름이 다를 수 있음; `convert-gpu-to-nvvm` 은 위 PTX 실험에서 `gpu.module(...)` 안에서는 동작). 결론 가능한 것은 "일부 패스가 파이썬에서 호출 가능" 까지. + +### 1.2 xdsl + +- [V] `uv pip install xdsl` -> xdsl 0.72.0 (PyPI 2026-10-02 릴리스). 순수 파이썬, 의존성 `immutabledict`, `ordered-set`, `typing-extensions` 셋뿐. Requires-Python >=3.10, classifier 3.14 까지. **py3.14t venv 에서도 설치/임포트되고 `sys._is_gil_enabled()` = False** [V]. 3.15t 는 시험하지 못함 (순수 파이썬이라 [I] 가능성 높음). +- [V] 라이선스: PyPI METADATA 는 "Apache License v2.0 with LLVM Exceptions". GitHub API 의 SPDX 는 NOASSERTION (표기 불일치, 사소함). GitHub star 595, 최근 push 2026-10-03, PyPI 릴리스 126 개, 최근 8 개가 2026-05-29 ~ 2026-10-02 사이 (약 2~4 주 간격) [V]. +- [V] dialect 목록 (`pkgutil.iter_modules(xdsl.dialects.__path__)`): acc, affine, arith, arm, arm_neon, bufferization, builtin, cf, comb, complex, csl, dlti, emitc, func, **gpu**, linalg, llvm, math, memref, memref_stream, ml_program, omp, pdl, ptr, riscv*, scf, snitch*, stencil, tensor, tosa, **transform**, ub, **vector**, wasm, x86*, 등. **`spirv` dialect 없음**: `grep -ril "spirv\|spir-v" site-packages/xdsl` 결과 0 건 [V]. +- [V] 백엔드 (`xdsl.backend`): `llvm` (xdsl -> LLVM IR 텍스트 변환, 약 950줄), `wgsl/wgsl_printer.py` (252줄), `riscv`, `x86`, `csl`, **`mps` 는 스텁** (`print_to_mps` 가 `NotImplementedError("MPS backend not yet implemented")`, AIR 비트코드를 목표로 한다고 docstring). `xdsl-opt` 의 대상 목록: csl, llvm, mps, riscv-asm, wgsl, x86-asm. SPIR-V, MSL, PTX 방출기는 없음. +- [V] gpu dialect(796줄)는 launch/func/module/global_id/barrier/subgroup_id 등 기본 op 위주이며, `subgroup_mma` 계열 op 이름은 상위 60 개 심볼 출력에서 확인되지 않음 [V 한계: 전체 grep 하지 않음]. 협력 행렬 관련 dialect 는 `grep` 상 없음. + +## 2. 선택지 비교 + +### 옵션 1. 완전 자체 IR + 자체 방출기 + +- 대상별 경로 [I]: + - SPIR-V: 우리 IR -> SPIR-V 워드를 직접 방출(binary writer 수백 줄 + 구조화 제어 흐름(OpLoopMerge/OpSelectionMerge), 메모리 모델, Capability/Extension 선언, 협력 행렬 `OpCooperativeMatrix*KHR`). `spirv-val` 로 검증 가능. + - MSL: 같은 IR 에서 텍스트 방출. 어느 옵션이든 MSL 은 우리가 쓴다(아래 참조), 자체 IR 에서는 **가장 자연스럽다**. + - WGSL: 텍스트 방출. SPIR-V 로 가는 것보다 단순. + - PTX: 텍스트 방출(작은 부분집합) 또는 LLVM 에 의존. 자체 PTX 방출은 레지스터 할당/주소 공간이 일. + - CPU SIMD: C(벡터 확장)로 방출, 이는 §4.3 에서 이미 정한 `typed IR -> C` 와 합칠 수 있다 [R typedpython.md §4.3]. +- 타일 변환: **전부 직접.** 레이아웃 추론, 소프트웨어 파이프라인, swizzle, autotune 대체물. 이것이 가장 큰 일이다 (이전 조사 §5.5). +- 도구 무게: 최소. 순수 파이썬 코드, 3.15t 문제 없음 [I]. +- 위험: 수년 걸리는 변환 인프라를 혼자 쌓는다. 반면 MSL/SPIR-V 직접 제어 가능, 디버깅이 쉽다. +- 첫 슬라이스 (f16 elementwise 또는 softmax -> SPIR-V -> `vulkan.rs` 디스패치 -> CPU 참조와 비교): 파이썬 AST -> 타입 IR(어차피 필요), SPIR-V 방출 약 800 ~ 1500 줄, 호스트 하니스. 추정 2 ~ 4 주(1인) [I, 측정 아님]. f16 은 `Float16` + `StorageBuffer16BitAccess` capability 필요(기기 의존). +- 타일 matmul + cooperative matrix: IR 에 tile 타입과 `mma` op, `OpCooperativeMatrix*` 방출, 공유메모리 타일링, 정밀한 레이아웃. 추정 2 ~ 4 개월 [I]. Adreno/Mali 에서는 기본값 아님(선택 경로) [V 이전 조사, llama.cpp PR 요약]. + +### 옵션 2. MLIR (upstream, 파이썬 바인딩, C++ 코어) + +- 획득 [V §1.1]: PyPI 에 없음. `llvm/eudsl` release 의 nightly wheel(매일, cp314t 포함, macOS arm64 / Linux x86_64+aarch64 / Windows x86_64). 커밋 해시가 버전에 붙는 **nightly 스냅샷**이라 안정 릴리스 번호가 없고 고정하려면 특정 날짜 wheel URL 을 lockfile 에 박아야 한다 (uv 의 `--find-links` 또는 URL 의존성). 직접 빌드는 LLVM 전체 빌드(`MLIR_ENABLE_BINDINGS_PYTHON=ON`, nanobind) 이며 시간/디스크가 크다 [I, 빌드해 보지 않음]. +- 버전 churn: 매일 갱신되는 upstream main 추종. Python 바인딩 API, dialect op 이름, 패스 옵션이 자주 바뀐다 (예: 검색에서 확인된 SPIR-V KHR CooperativeMatrix Load/Store Stride 옵션화 PR #214194, 협력 행렬 operand 수 수정 PR #118014) [V 검색요약]. 완화: 특정 날짜 wheel 로 고정하고 분기별로 올리는 정책 [I]. +- 3.15t: **wheel 없음** (cp314t 까지) [V]. 기반이 nanobind 라 소스 빌드로 3.15t 가능성은 있으나 미확인 [I]. 사용자 목표가 3.15t 이므로, 컴파일러(빌드 타임)를 3.14t 또는 GIL 파이썬 별도 인터프리터에서 돌리고 앱 런타임과 분리하는 것이 현실적 [I]. 공식 문서가 free-threading 에서 GPU dialect/PassManager 동시 사용을 안전하지 않다고 명시한 점도 단일 스레드 컴파일로 우회 [V 문서]. +- 대상별 경로: + - SPIR-V: gpu/vector/arith/memref/scf -> `convert-gpu-to-spirv` -> `spirv` dialect -> `gpu-module-to-binary` 로 직렬화 [V 부분: 각 단계 개별 실행. 연결은 접합 코드 필요]. 협력 행렬: `gpu.subgroup_mma_*` 가 `spirv.KHR.CooperativeMatrixMulAdd` 로 내려가는 변환이 있다는 포럼 글 [V 검색요약], `spirv.KHR.CooperativeMatrix*` op 은 설치본에 존재 [V]. 실제로 f16 협력 행렬 matmul 이 변환을 통과해 Adreno/Mali 에서 도는지는 **검증 못함**. + - MSL: **upstream 에 MSL 타깃 없음**. 우리가 확인한 설치본에서 dialect 목록(spirv, gpu, nvvm, amdgpu, emitc 등)에 metal/msl 이 없다 [V 목록 확인]. 외부 사례만 있음: Triton 외부 백엔드(triton-ext PR #126, bledden/triton-msl)가 TTGIR 에서 MSL 을 방출하는 패스를 가진다 [V 검색요약]. 가능한 길 둘: (a) `emitc` dialect 로 C++ 형 텍스트를 뽑는 구조를 흉내 내 **MSL 프린터를 우리가 작성**, (b) SPIR-V -> SPIRV-Cross -> MSL (협력 행렬은 simdgroup_matrix 8x8 float 로만 제한되고 최근 initial 지원 단계 [V 검색요약, SPIRV-Cross PR #2645]; Metal 4 cooperative tensor / matmul2d 는 도달 불가 [V 이전 조사]). 따라서 Apple 은 어느 쪽이든 MSL 방출기를 직접 쓴다 [I]. + - WGSL: upstream 에 WGSL 타깃 없음 [V 목록]. SPIR-V -> naga 또는 직접 방출. + - PTX: **되는 것을 확인** [V §1.1, `t_ptx.py`]. 가장 성숙한 경로. + - CPU SIMD: `vector` + `llvm` dialect -> LLVM IR -> ExecutionEngine/오브젝트 [V: `mlir.execution_engine` 임포트됨. 실제 NEON 오브젝트 방출은 시험 안 함]. 또는 `emitc` 로 C. iOS 앱엔 LLVM 을 싣지 않고 빌드 타임에 오브젝트/라이브러리만 [I]. +- 타일/레이아웃/파이프라인 변환: **여기가 MLIR 의 최대 이점.** `linalg`(structured ops) + `transform` dialect(`structured.tile_using_for`, vectorize 등), `vector.contract` 패턴, `gpu` dialect 의 mma/예약 [V 검색요약 + 설치본에 `_structured_transform_ops_gen`, `_gpu_transform_ops_gen`, `_nvgpu_transform_ops_gen` 존재 [V]]. 소프트웨어 파이프라이닝(cp.async 급) 은 nvgpu 쪽에 일부, Vulkan 쪽은 IREE 가 가진 별도 코드젠(IREE 의 SPIR-V 파이프라인은 upstream 이 아님 [V iree.dev 요약])이라 upstream 만으로 Vulkan 에서 Triton/IREE 급 matmul 성능이 나온다는 보장은 없다 [I]. +- 프런트엔드 접합: 파이썬 AST -> MLIR 를 `ir.Module`/`OpBuilder` 로 구성. "valid Python 그대로 실행" 요건은 프런트엔드가 AST 를 읽는 방식이므로 충돌하지 않는다 [I]. +- 도구 무게: wheel 66 ~ 76 MB / 설치 253 MB, 빌드 타임 전용이므로 앱 크기와 무관. 단 uv/pypackpack 설치 흐름에 PyPI 가 아닌 외부 index(find-links) 가 들어가고 날짜 고정이 필요하다 [V + I]. +- 위험: (1) nightly 스냅샷 의존과 API churn, (2) 3.15t wheel 부재, (3) eudsl 이 자체를 "alpha" 로 표기 [V], (4) MSL/WGSL 은 어차피 직접 방출, (5) 디버깅이 C++ 패스 안에서 일어나 에러 메시지가 불친절 [I], (6) wheel 이 단일 메인테이너 프로젝트(makslevental -> llvm/eudsl) 의 유지에 달림 [I]. +- 첫 슬라이스: 접합 코드(gpu.module 에 spirv.module + target_env/vce 부착) + 프런트엔드 -> MLIR 빌더 + 형상/버퍼 ABI 정리(`spirv.interface_var_abi`, push constant 는 MLIR 에서 `spirv.StorageClass PushConstant` 구성 필요 [I]). 추정 2 ~ 4 주 [I]; 환경/버전 고정과 f16/ABI 문제 때문에 옵션 1 과 비슷하거나 약간 더 걸릴 수 있다. 이득은 슬라이스 이후. +- 타일 matmul + cooperative matrix: 변환 파이프라인을 upstream 패스로 조립. Vulkan KHR 협력 행렬 경로는 존재하나 성능 튜닝 정도는 모름. 추정 1 ~ 3 개월, 하지만 위험이 옵션 1 보다 "변환이 안 맞을 때 고칠 수 없음" 쪽에 쌓인다 [I]. + +### 옵션 3. xdsl (순수 파이썬 MLIR 호환) + +- [V §1.2] 설치 쉬움(PyPI, 의존 3개, 3.10 ~ 3.14t), 활발(2~4 주 릴리스), 라이선스 Apache-2.0 with LLVM Exception. 3.15t 는 미시험이나 순수 파이썬이라 [I] 문제 가능성 낮음. +- MLIR 텍스트 호환: `xdsl-opt` 로 MLIR 텍스트를 읽고 쓴다(프린터/파서). 따라서 **xdsl <-> MLIR(mlir-opt, 위 wheel) 상호운용을 텍스트로 연결**할 수 있다 [I, 두 도구를 실제로 연결해 보지 않음]. 즉 옵션 3 은 옵션 2 와 배타적이지 않다: 프런트엔드/고수준 변환은 xdsl, SPIR-V 직렬화나 NVPTX 는 MLIR wheel 에 텍스트로 넘기는 혼합 구성이 가능 [I]. +- 대상별 경로: + - SPIR-V: **방출기 없음, spirv dialect 도 없음** [V]. 텍스트를 MLIR 에 넘겨야 하거나 SPIR-V 방출을 직접 써야 한다. + - MSL: 없음. `mps` 백엔드는 AIR 비트코드를 목표로 한 NotImplemented 스텁 [V]. + - WGSL: 있음(252줄) 단 범위는 작음(소스에서 `gpu.FuncOp`, `memref.StoreOp` 처리 등 기본 연산) [V 부분: 코드 헤드만 확인, 지원 범위 전체는 미확인]. + - PTX: 없음. LLVM IR 텍스트 변환(`xdsl.backend.llvm`)은 있어 `llvm` 의 NVPTX 로 이어 붙일 수 있으나 llvmlite 등 별도 필요 [I]. + - CPU SIMD: `vector`, `x86`, `arm_neon`, `riscv`, `llvm` 백엔드 등 있음. 실사용 성숙도는 미확인. +- 변환: `transform` dialect(1042줄) 와 `vector`(1470줄), `linalg` 가 있으나 upstream MLIR 의 `structured.*` 변환 패스 수준인지 확인 못 함. 협력 행렬/타일링 패스 존재는 확인 못 함 [V 한계]. 패스를 파이썬으로 쓰므로 새 패스 작성/수정은 쉬움(`pattern_rewriter`) [I]. +- 성숙도: 연구/컴파일러 교육 중심 프로젝트(스텐실, CSL, RISC-V/Snitch 등) 이며 GPU 코드젠 프로덕션 사례는 확인 못함 [I, 근거: dialect 구성]. +- 위험: 필요한 후단(SPIR-V/MSL/PTX 방출, 협력 행렬 dialect)이 거의 비어 있어, 옵션 1 에서 "IR 클래스 + 패스 인프라 + 프린터" 를 xdsl 로 빌려 오는 정도의 이득. IR 인프라(SSA, 파서, verifier, 패턴 리라이터, 정확성 검사)를 공짜로 얻는 점이 장점 [I]. +- 첫 슬라이스: 옵션 1 과 같은 SPIR-V 방출기를 어차피 써야 하므로 추가 이득은 IR 인프라 재사용뿐. xdsl 학습 비용 + 방출기 직접 작성 -> 2 ~ 4 주 [I]. Cooperative matrix: spirv dialect 를 xdsl 에 추가(IRDL 로 정의 가능 [I]) + 직렬화기 작성 필요. 옵션 1 대비 이득 작음. + +### 기타 (간단히) + +- Triton 의 MLIR dialect(TTIR/TTGIR) 재사용: 서브모듈/LLVM 포크에 묶여 있고 JIT 전제, Apple MSL 은 외부 포크(triton-ext)가 TTGIR -> MSL 을 했다 [V 검색요약]. 우리 파이썬-서브셋 프런트엔드와 TTGIR 의 레이아웃 인코딩을 쓰려면 Triton 빌드 전체가 따라온다 [I]. 비추천. +- IREE: SPIR-V 코드젠(Mali Valhall, Adreno 640+ 대상 triple) 과 Vulkan 런타임을 가진 AOT 컴파일러 [V iree.dev 요약]. 입력은 모델 단위(linalg/StableHLO)이고 산출물은 `.vmfb`(flatbuffer) 이며 SPIR-V 만 라이브러리로 뽑아 쓰는 공식 경로는 확인하지 못했다 [V 부재는 검색 한계]. 타일링/매트릭스 파이프라인이 가장 성숙한 오픈 구현이므로 **알고리즘 참조 대상**, 의존 대상은 아님 [I]. +- Mojo: 닫힘, MAX 가 Apple GPU 초기 [V 이전 조사]. +- SPIRV-Cross(MSL): 협력 행렬 초기 지원, simdgroup_matrix 는 8x8 float 계열만 [V 검색요약]. 즉 SPIR-V 경유 Apple 은 f16 matmul 에서 불리할 수 있고 Metal 4 tensor ops 는 불가 [V 이전 조사 + I]. + +## 3. 요약 표 + +| 항목 | 1. 자체 IR | 2. MLIR wheel | 3. xdsl | +|---|---|---|---| +| SPIR-V 방출 | 직접 작성 | **있음** (gpu/spirv dialect + `gpu-module-to-binary`, 접합 필요) [V] | 없음 [V] | +| MSL | 직접 (자연스러움) | upstream 없음, 직접 [V] | 없음, mps 는 AIR 스텁 [V] | +| WGSL | 직접 | upstream 없음 | 있음(소형) [V] | +| PTX | 직접(작게) | **있음, 실행 확인** [V] | 없음 (llvm 텍스트 경유 [I]) | +| CPU SIMD | C 벡터 확장 | vector+llvm / emitc [I] | arm_neon/x86/llvm 있음 [V 존재] | +| 타일/레이아웃/파이프라인 | 전부 직접 | linalg+transform+vector+gpu 가 큼 [V 존재, 성능은 미확인] | transform/vector 있음, 수준 미확인 | +| 설치 | 없음 | wheel 66~76MB, PyPI 아님, nightly find-links [V] | `uv add xdsl` [V] | +| 3.14t | 해당 | **wheel 있음, GIL 유지 off** [V] | **됨** [V] | +| 3.15t | 문제 없음 [I] | **wheel 없음** [V] | 가능성 높음 [I] | +| 버전 churn | 없음 | 높음 (매일) [V] | 중 (2~4 주 릴리스, 0.x) [V] | +| 첫 슬라이스 | 2~4 주 [I] | 2~4 주 [I] | 2~4 주 [I] | +| 타일 matmul + coop mat | 2~4 개월 [I] | 1~3 개월 (고장 시 수정 불가 위험) [I] | 2~4 개월 [I] | + +## 4. 권고 (불확실성 명시) + +1. **IR 의 정본은 우리가 소유한다(옵션 1 의 IR), 단 MLIR 호환 형태로 설계한다.** 이유: (a) §2.1 요건과 프런트엔드는 우리 몫이며 어떤 외부 IR 도 대신하지 못한다 [I], (b) MSL, WGSL 은 MLIR 을 써도 직접 써야 하므로 MLIR 이 줄여 주는 방출기는 SPIR-V 와 PTX 둘뿐이다 [V], (c) 3.15t wheel 이 없고 nightly 고정이라 도구 체인의 기초로 삼기에는 변동이 크다 [V]. 이 판단은 "방출기가 일의 대부분" 이라는 가정에 의존한다 [I]. +2. **MLIR wheel 은 "SPIR-V 와 PTX 직렬화기/검증기, 그리고 비교 오라클" 로 선택적 사용을 우선 시험한다.** 우리 IR 을 MLIR 텍스트로 내리는 얇은 방출기(수백 줄)만 쓰면, `convert-gpu-to-spirv` + `gpu-module-to-binary` 로 SPIR-V 를, `gpu-module-to-binary{format=isa}` 로 PTX 를 얻는다 [V 각 단계 개별]. 막히면 직접 SPIR-V 방출로 후퇴할 수 있어 위험이 낮다. 3.15t 와는 별도 프로세스(3.14t 또는 GIL 인터프리터) 로 분리 실행 [I]. +3. **타일 변환(tiling, 협력 행렬, pipelining) 에서 MLIR 의 transform/linalg/vector 를 쓸지는 슬라이스 2 이후 별도 시험으로 결정.** 지금은 약속하지 않는다. 이 영역이 MLIR 의 진짜 이점이지만 Vulkan 에서의 성능은 IREE 쪽 코드젠에 있다 [I, V 검색요약]. +4. **xdsl 은 채택하지 않되 IR/패스 인프라 설계의 참고, 그리고 MLIR 텍스트 상호운용 후보로 남긴다.** 후단이 비어 있어 SPIR-V/MSL/PTX 에 이득이 없다 [V]. +5. **Apple 은 MSL 직접 방출.** 이전 조사와 동일 결론이고 이번에도 어느 옵션도 대신하지 않는다 [V + I]. + +미결/불확실: (a) `gpu.func` 하향 결과를 `gpu-module-to-binary` 로 연결하는 접합이 실제로 쉬운지 (실행 안 함), (b) f16/협력 행렬 SPIR-V 가 실기(Adreno, Mali)에서 도는지, (c) wheel 이 NEON/x86 CPU 오브젝트를 만들 수 있는지(LLVM 타깃 구성 미확인; NVPTX 는 확인), (d) 3.15t 에서의 MLIR 소스 빌드 가능성, (e) eudsl wheel 의 장기 유지, (f) 시간 추정은 모두 측정 아닌 추정. + +## 5. 다음 시험 (구체적 첫 슬라이스) + +1. `t_spirv.py` 를 확장: 하향된 `spirv.module` 을 `gpu.module [target_env]` 안에 옮기고 `requires` 를 붙여 직렬화, `spirv-val` 로 검증 (`spirv-val` 은 이 환경에 설치하지 않았음). +2. 그 `.spv` 를 `torch_c/shaders/` 의 체크인 모양대로 넣고 `dispatch_kernel_words` 경로로 실행, CPU 참조(torchnative flash.rs 식 허용 오차 비교)와 대조. 이 단계는 torchnative 쪽 빌드가 필요하므로 이번 조사에서는 하지 않았다. +3. 같은 커널을 직접 방출한 SPIR-V 와 크기/명령어/기기 호환성 비교(옵션 1 대비 MLIR 산출물의 위험 평가). + +## 6. 이 조사 방법이 찾지 못하는 것 + +wheel 설치와 개별 패스 실행으로 "존재와 단일 단계의 동작" 만 확인했으므로, 연결된 전체 파이프라인의 정확성, 실기 GPU 드라이버 동작, SPIR-V 산출물의 성능, 장기 API 안정성, 그리고 xdsl/MLIR 변환 패스의 실제 품질은 찾지 못한다 (시간 추정도 측정이 아니다). diff --git a/docs/investigations/typedpython-ownership-memory.md b/docs/investigations/typedpython-ownership-memory.md new file mode 100644 index 000000000..ab4b9f940 --- /dev/null +++ b/docs/investigations/typedpython-ownership-memory.md @@ -0,0 +1,292 @@ +# TypedPython: ownership inference 와 메모리 (Rust 대비) 조사 + +결론의 반영: `docs/design/typedpython.md` §4.3.5, 결함 #193 (2026-10-05). 메인테이너 결정: D1 예, D2 예(조건부), D3 예(조건부), D4 아니오, D5 결함 수정, D6 예(앱 배포 빌드만), D7 예, D8 지금은 아니오. 측정에 쓴 스크립트와 원자료(`.tmp/research/`)는 커밋하지 않았다. + +날짜 2026-10-05. 태그: [V] 가져온 URL 또는 실행한 명령(근거 표기), [R] 로컬 파일을 읽음, [I] 추론. +측정 환경: Apple Silicon macOS (Darwin 25.5.0), CPython 3.14.7 (GIL), 3.14.7t, 3.15.0rc1 (GIL), 3.15.0rc1t. +설치는 `uv python install` 로 .tmp/research/pyinst 에 했다. 모든 스크립트와 원자료는 .tmp/research/ 아래에 있다 +(measure.py, measure.out, rssdelta.py, rssdelta.out, rss.out, cext/, rs/). + +## 0. 요약 + +1. 소유권 추론이 줄이는 것은 **시간**(refcount 연산, 할당)이고, 장기 상주 데이터의 **크기**는 거의 줄이지 못한다. + 장기 메모리는 객체 표현(박싱된 float 32 B, free-threaded 에서는 헤더 32 B)이 정한다 (B.1 표). +2. N-11 클래스를 non-GC 로 만들면 GIL 빌드에서 객체당 16 B(48 → 32 B)를 얻는다 [V cext/t.out]. 그러나 + **free-threaded 3.15t 에서는 0 B** 다 (GC 비트가 ob_gc_bits 로 헤더에 들어가 있어서, 48 == 48) [V cext/t.out]. + 우리 대상은 3.15t 뿐이므로 non-GC 의 메모리 이득은 사실상 없고, 얻는 것은 GC 추적·순회 시간뿐이다. +3. 큰 이득은 **float/int 슬롯을 객체 안에 unbox** 하는 것이다 (두 float 필드 객체 157 B → 약 48 B, 3.3x, 3.15t 기준). + 그러나 이는 `a.x is a.x` 를 거짓으로 만들고 [V], `PyType_FromSpec` 으로 컴파일러가 타입을 직접 만들어야 한다. + 둘 다 사용자 결정이 필요하다. +4. **free-threaded 에서는 N-12 의 "borrowed 읽기" 근거가 무너진다** (A.2 (4)). 현재 문서는 "GIL 을 놓지 않으므로 + 미지 코드 호출이 없으면 슬롯이 안 바뀐다" 에 기대는데, 3.15t 에서는 다른 스레드가 동시에 슬롯을 바꾼다. [I] +5. 설치 부작용: `uv python install` 이 `~/.local/bin/python3.14t`, `python3.15t` 심볼릭 링크 두 개를 저장소 밖에 + 만들었다 (규칙 2 위반, 의도하지 않음). 사용자 승인으로 2026-10-05 에 지웠다. 다음 조사는 `uv python install` 대신 저장소 안 venv 만 쓴다. + +## Part A. 소유권 추론 ("GC 를 스마트 포인터로") + +### A.1 선행 사례 조사 + +| 시스템 | 메커니즘 | 언어에 요구하는 것 | 보고된 수치 | 근거 | +|---|---|---|---|---| +| Koka, Perceus | 정확한(precise) RC: 마지막 사용 직후 drop, 소유 참조를 이동하고 나머지만 dup. drop specialization, reuse analysis(해제될 셀을 같은 크기의 새 할당에 재사용), FBIP | 순수 함수형. 불변 데이터, 즉시 평가라서 순환이 없고 순환이 생길 수 있는 곳(ref)이 정적으로 알려져 있음 (논문 2.7.4). 타입과 효과가 추적됨 | 할당 집약 벤치에서 OCaml, Haskell, Swift, Java 와 비교해 "outstanding"; 트리 삽입(rbtree)에서 순수 함수형 Koka 가 in-place C++ std::map 의 10% 이내. no-opt 대비 reuse 로 메모리 8% 적음(rbtree 계열) | [V] https://www.microsoft.com/en-us/research/wp-content/uploads/2020/11/perceus-tr-v1.pdf (pdftotext 로 읽음: perceus.txt 줄 63 부터, 841, 1141) | +| Lean 4 | 소유(owned)/빌림(borrowed) 파라미터, 빌림 추론 휴리스틱, reset/reuse(공유되지 않았으면 제자리 재사용), 값에 single-threaded/multi-threaded/persistent 태그 | 순수 함수형, 즉시 평가 | 기준 대비 상대 시간 (ablation, lean.txt 표 6): `-borrow` 를 끄면 binarytrees 약 1.14x, deriv 1.16x 느림; `-reuse` 를 끄면 const_fold 1.64x, rbmap 3.23x; `-ST`(원자 RC 강제) 는 binarytrees 1.22x ~ rbmap 1.71x. 단 표 숫자가 PDF 추출에서 일부 깨져 소수 둘째 자리는 신뢰하지 않는다 | [V] https://arxiv.org/abs/1908.05647 (PDF https://arxiv.org/pdf/1908.05647 를 읽음) | +| Lobster | 할당마다 단일 소유자(보통 처음 대입된 변수, 필드, 벡터 원소)를 고르고 이후 사용을 가능한 한 borrow 로 만든다. 다른 곳에서 소유하려 할 때만 증가를 삽입. 런타임 RC 는 폴백으로 남음 | 정적 타입. 프로그래머 개입은 대체로 불필요 | "런타임 refcount 연산의 약 95% 제거" | [V] https://aardappel.github.io/lobster/memory_management.html | +| Nim ARC/ORC | 소멸자와 move 기반. 정적 제어 흐름 분석으로 "복사원이 이후 쓰이지 않으면 move", cursor 추론으로 복사 대신 별칭. ORC 는 ARC 위에 순환 수집기(trial deletion). 순환이 불가능한 타입은 수집기가 간선으로 보지 않음, `{.acyclic.}` 로 프로그래머가 보증 가능, 거짓 보증은 누수만 일으킴 | 정적 타입, 값 의미론 | 수치 없음 (문서와 블로그에 벤치마크가 없다) | [V] https://nim-lang.org/docs/destructors.html (move, cursor), 순환 부분은 검색 결과 요약: https://nim-lang.org/docs/mm.html (검색 결과 목록에서 확인, 해당 페이지를 직접 내려받지는 않음. 2차 확인 필요) | +| Swift ARC, OSSA | SIL 에서 값에 ownership 종류(owned / guaranteed / none / unowned)를 부여해 retain/release 쌍을 최적화기가 안전하게 제거. 변수 수명 전략 3종(strict, lexical, optimized) | 값 타입 위주, 클래스는 RC. 순환은 weak/unowned 를 프로그래머가 써야 함 | 문서에 수치 없음. Lean 논문 표에서 Swift 5.1.1 은 binarytrees 에서 Lean 의 약 5.4x 시간, 59% 가 GC(RC) 시간으로 표기됨 | [V] https://github.com/swiftlang/swift/blob/main/docs/SIL/Ownership.md (ownership 종류), 수치는 [V] lean.txt 의 비교 표 | +| Mojo | 인자 규약: 기본은 불변 참조(read), `mut` 가변 참조, `var` 소유권 이전(`^` 로 명시 이동), `out`/`deinit` 특수. ASAP 소멸: 마지막 사용 직후 소멸(스코프 끝이 아님), origin 이 참조의 수명을 추적 | 소유권을 시그니처에 **직접 적는다** (추론이 아니라 선언) | 수치 없음 | [V] https://mojolang.org/docs/manual/values/ownership , https://mojolang.org/docs/manual/values/lifetimes | + +읽은 결과 공통점 [I]: 이득이 큰 시스템(Koka, Lean, Lobster)은 **불변 데이터 또는 단일 소유자 가정** 위에 있다. +파이썬 객체는 변경 가능하고 별칭이 자유로워서 (1) borrowed 로 증명할 수 있는 범위가 좁고, (2) 순환을 정적으로 배제할 수 없다. +Mojo 처럼 소유권을 선언하게 하는 길은 "새 문법 없음" 에 막힌다. + +### A.2 우리 설계에 사상 + +#### (1) 컴파일된 함수 사이의 owned/borrowed 파라미터와 반환 추론 + +- **진입 규약.** 인터프리터에서 들어오는 인자는 vectorcall 규약상 호출자가 소유한다. 즉 컴파일된 함수 입구에서는 + 모든 객체 파라미터가 borrowed 다. 그러므로 "owned 로 받는다" 는 컴파일된 함수끼리의 내부 ABI 에서만 의미가 있다. [I] +- **추론 규칙(Lean 식).** 파라미터는 borrowed 로 시작하고, 함수 안에서 (a) 슬롯·전역·컨테이너에 저장되거나 (b) 반환되거나 + (c) owned 파라미터 자리로 넘겨질 때만 owned 로 승격한다. 승격 지점에서만 INCREF 를 한 번 낸다. 반환은 항상 owned (새 참조)로 + 둔다. borrowed 반환은 수명 추적 없이는 증명할 수 없어 하지 않는다 (Lean 도 반환은 owned). [I] +- **현재 설계와의 관계.** 4.3.4 나 절의 "빌린 읽기" 는 바로 이 규칙의 특수 경우다 (`check(node.left)`: 부모가 소유되고 callee 가 + 슬롯을 쓰지 않으면 무연산). IR 의 N-9 소유 규칙(OBJ 참조는 하나의 소유 규칙)과 순수성 분석이 이미 필요한 정보를 갖고 있다. [R] +- **기대 효과.** Lobster 95%, Lean ablation 1.14 ~ 1.16x 는 **함수형 언어에서 RC 가 지배 비용인 경우**다. 우리는 refcount 쌍이 + 3 ~ 4 ns (7절 R-1 측정)인 반면 객체 할당과 해제는 이보다 크다 (바이너리 트리의 1.7x 격차가 할당 때문이라는 #125 가설이 + 있고 아직 프로파일 전이다 [R 설계 문서 4.3.4]). 따라서 refcount 생략은 가상 객체·영역 할당보다 **2차적**이라고 본다. [I] + 이 순서 판단은 측정으로 확인하지 못했다. +- **abi3t 의 영향.** 3.15 헤더에서 abi3t 는 `Py_INCREF`/`Py_DECREF`/`Py_REFCNT` 를 함수 호출로 만든다 [V src/refcount.h.main 줄 + 101, 159, 260, 330 의 주석]. 그래서 쌍 하나를 빼면 호출 둘을 아낀다. 단 limited API(0x030c) 로 GIL 3.14 에서 측정한 쌍 비용은 + 4.12 ns 로 인라인 3.96 ns 와 큰 차이가 없었다 [V cext/rc.out]. abi3t + 3.15t 조합은 컴파일하지 않았다 (측정 안 함). +- **관측성.** refcount 는 `sys.getrefcount` 와 미지 코드 호출에서만 보인다. 미지 호출 앞에서 쌍을 복원하면 관측 불가. [R 4.3.4 나] + 따라서 이 추론 자체는 **사용자 결정 없이** 동일성 규칙 안에서 구현 가능하다 (단 A.2 (4)의 free-threaded 조건 아래에서). + +#### (2) N-11 클래스가 비순환임을 증명하고 `Py_TPFLAGS_HAVE_GC` 없이 만들기 + +사실 확인(전부 실행): + +| 항목 | GIL 3.14.7 | 3.14.7t | 3.15.0rc1t | 근거 | +|---|---|---|---|---| +| 파이썬 `class S: __slots__=("x","y")` 인스턴스 `gc.is_tracked` | True | True | True | [V cext/t.out] | +| `__slots__ = ()` 빈 클래스 인스턴스 `gc.is_tracked` | True | True | True | [V cext/t.out, S0] | +| `PyType_FromSpec` 으로 만든 non-GC 타입 인스턴스 `gc.is_tracked` | False | False | False | [V cext/t.out, NonGcObj] | +| 그 인스턴스 `gc.get_objects()` 에 나타남 | 아니오 | 아니오 | 아니오 | [V] | +| `sys.getsizeof` : 파이썬 클래스(슬롯 2) / non-GC 타입 | 48 / 32 | 48 / 48 | 48 / 48 | [V] | +| 슬롯 2개 non-GC 타입에서 자기참조 1000개를 `gc.collect()` 후 dealloc 횟수 | 0 (누수) | 0 | 0 | [V]; GC 타입은 1000 | + +- **파이썬 `class` 문으로는 non-GC 가 불가능하다.** `type_new_alloc` 이 모든 힙 타입에 `Py_TPFLAGS_HAVE_GC` 를 건다 + [R src/typeobject.c.314 줄 4183 ~ 4199, 주석 "All heap types need GC"]. 실제로 빈 슬롯 클래스도 추적된다 (위 표). [V] +- **따라서 컴파일러가 타입을 `PyType_FromSpec`(또는 `PyType_FromMetaclass`)로 직접 만들어야 한다.** 스펙이 주는 플래그가 그대로 쓰여 + `HAVE_GC` 를 뺄 수 있음을 실험으로 확인했다 [V cext/ngc.c, t.out]. 문서도 GC 플래그가 켜지는 조건을 + "순환에 참여할 수 있는 참조를 소유하거나 traverse/clear 를 구현할 때" 로 설명한다 [V https://docs.python.org/3.14/c-api/typeobj.html]. +- **뒤따르는 관측 차이.** 컴파일러가 만든 타입은 클래스 사전에 `__new__` 가 더 있고 `__slots__` 속성이 없다 (`hasattr(T,"__slots__")` + False) [V]. `__flags__`, `__basicsize__`, `sys.getsizeof`(GIL 에서 16 B 작음), `gc.is_tracked`, `gc.get_objects` 가 파이썬 클래스와 + 다르다. `__slots__`·`__init__`·`__module__`·`__qualname__`·`__doc__` 를 복사해 대부분은 맞출 수 있지만 `gc` 관련 네 항목은 **정의상** + 달라진다. 컴파일된 모듈이 소스를 먼저 실행해 만든 원래 클래스와 이 타입을 어떻게 바꿔치기할지 (전역 이름 교체, deopt 용 인터프리터 + 본문이 쓰는 클래스) 도 설계 문제다. [I] +- **비순환 증명의 한계(핵심).** + - 주석의 타입은 런타임에 강제되지 않는다. 슬롯이 `PyObject*` 이면 인터프리터 쪽 코드가 `n.next = n` 처럼 무엇이든 넣을 수 있다. + non-GC 타입 안의 순환은 영원히 수집되지 않고 (위 표 마지막 행), **다른 객체의 `__del__`/weakref 콜백도 영원히 안 돈다**. 이는 GC 시점 + (결정 (b))이 아니라 **수집 자체가 없는** 것이라 동일성 규칙 밖으로 나가는지 별도 판단이 필요하다. [I] + - `Node(left: Node, right: Node)` 처럼 타입이 자기 자신을 닫는 재귀 클래스는 타입 그래프에서 이미 순환 가능하므로 증명 대상이 아니다 + (binary-trees 의 Node 는 안 된다). 비순환으로 증명되는 것은 필드 타입이 float/int/bool/str/None 이거나, 다른 비순환 N-11 클래스의 + DAG 인 클래스뿐이다. [I] + - 그 경우도 `PyObject*` 슬롯에는 런타임 타입 위반을 막는 setter 가 없다. 안전하려면 슬롯이 **참조를 못 담는** 표현(unbox 된 double/i64)이거나 + 저장 시 정확한 타입 검사를 해야 한다. 검사하면 CPython 이라면 성공했을 잘못된 타입 대입이 예외가 된다 (결정 필요, B.5 D3). + - 서브클래스: `BASETYPE` 를 켜고 파이썬에서 상속하면 서브타입은 GC 가 되지만 기반 타입의 `tp_traverse` 가 없어 기반 슬롯의 참조를 순회하지 못한다. + 그래서 non-GC 타입은 final 이어야 하고 (`class Sub(Node)` 가 CPython 과 달리 `TypeError`), 이것도 관측 차이다. [I, 실험 안 함] +- **절약 추정.** GIL: 객체당 16 B (슬롯 2개 객체 48 → 32 B, -33%) [V]. 3.15t: 0 B [V]. 시간: GC 할당 카운터·추적·untrack 이 사라진다 (측정 안 함). + +#### (3) refcount 1 일 때 제자리 재사용(Perceus)과 관측성 + +- **가능한 곳.** 컴파일된 함수가 **직접 소유한** (자기가 만들었거나 owned 로 돌려받은) N-11 객체가 죽는 지점 바로 뒤에 같은 클래스 객체를 + 만들 때 (루프에서 `n = Node(...)` 를 반복 대입하는 형태). 인자로 들어온 객체는 borrowed 이므로 `Py_REFCNT == 1` 이어도 재사용하면 안 된다 + (호출자의 지역 변수가 그 하나를 쥐고 있다). [I] +- **CPython 3.14 의 우연한 해석 차이.** 인터프리터의 stackref borrow(LOAD_FAST_BORROW) 때문에 인터프리터 쪽 refcount 는 의미상 참조 수보다 작을 수 있다. + 우리는 공개 API 의 `Py_REFCNT` 만 쓰므로 "내가 소유한 참조가 유일" 만 쓰면 된다. [I, 이 부분 문서 확인 안 함: whatsnew 3.14 에는 해당 서술이 없었다 [V]] +- **free-threaded 의 유일성 검사.** 내부 판정은 `owned by current thread && ob_ref_local == 1 && ob_ref_shared == 0` [R src/pycore_object.h.314 줄 184 ~ 196]. + 공개 형태 `PyUnstable_Object_IsUniquelyReferenced` 는 비 stable ABI 헤더(cpython/object.h)에 있다 [R src/object.h.314 줄 493]. abi3t 에서는 쓸 수 없고, + 쓰려면 "내가 소유한 유일 참조" 를 컴파일러 증명으로 보장하고 `Py_REFCNT(o) == 1` 을 보조 확인으로 쓴다. [I] +- **관측 지점(전부 [I]).** + 1. `id()`: 같은 주소가 다시 나오지만 CPython 도 해제 직후 할당에서 같은 주소를 자주 재사용하므로 유효한 실행이다. + 2. `gc.get_objects()` 순서와 세대 소속: 재사용 객체는 옛 위치와 세대에 남는다 (새 할당은 젊은 세대 끝). GC 시점 결정 (b) 와 같은 범주로 볼 수 있으나 `get_objects(generation=0)` 은 직접 관측 가능. + 3. `tracemalloc.get_object_traceback`: 재사용 객체는 이전 할당 위치를 보고한다. + 4. `__del__`/weakref 클래스: 이미 N-12 대상에서 제외됨. + 5. 자식 refcount: 슬롯 덮어쓰기에서 자식 decref 는 원래 해제와 같은 시점에 한다. +- **절약.** 할당·해제 호출과 0 채우기 생략. 메모리 크기에는 영향 없음. Lean ablation 에서 reuse 가 const_fold 1.64x, rbmap 3.23x [V] 이지만 이는 불변 함수형 트리 재구성 패턴이고, 파이썬의 변경 가능 객체에는 같은 비율이 나온다는 근거가 없다. [I] + +#### (4) free-threaded 3.14t/3.15t + +- **헤더 배치** [V src/object.h.top314 줄 152 ~ 163]: + + uintptr_t ob_tid; // 소유 스레드 id (GC 와 trashcan 이 연결 리스트·gc_refs 로도 재사용) + uint16_t ob_flags; + PyMutex ob_mutex; // 객체별 락 + uint8_t ob_gc_bits; // GC 상태 (PyGC_Head 를 대체) + uint32_t ob_ref_local; // 소유 스레드의 비원자 카운트 + Py_ssize_t ob_ref_shared;// 다른 스레드의 원자 카운트 (+ 상태 비트) + PyTypeObject *ob_type; + + GIL 빌드의 16 B 헤더(+ GC 객체는 앞에 16 B GC 헤더)에 비해 free-threaded 는 32 B 헤더이고 GC 헤더가 없다. 측정에서 float 24 → 40 B, `object()` 16 → 32 B + [V measure.out]. 그래서 GC 객체는 크기가 같고 (48 == 48), 비 GC 객체(int, float, non-GC 타입)는 free-threaded 에서 커진다. +- **biased refcount.** 소유 스레드는 비원자 `ob_ref_local`, 다른 스레드는 원자 `ob_ref_shared` [V PEP 703 https://peps.python.org/pep-0703/]. + 측정(INCREF+DECREF 한 쌍, 3.15t): 생성한 스레드 소유 객체 3.19 ns, **다른 스레드가 만들고 끝난 객체 14.24 ns**, 불멸(None) 0.63 ns. + GIL 3.14: 3.96 ns, 불멸 0.35 ns [V cext/rc.out, 로드 평균 약 2, N=5천만, 3회 중 최소]. 즉 스레드 사이를 오가는 객체에서는 쌍 하나가 4.5배 비싸다. +- **deferred refcount.** 최상위 함수, 코드 객체, 모듈, 메서드처럼 여러 스레드가 자주 접근하는 객체는 스택 push/pop 에서 RC 를 건너뛰고 GC 때 참 카운트를 계산한다 + [V PEP 703]. N-11 인스턴스에 적용 가능한지는 확인하지 못했다. +- **immortalization.** `ob_ref_local == UINT32_MAX` 이면 INCREF/DECREF 는 무연산 [V PEP 703, src/refcount.h.314 줄 76, 110]. 할당기는 mimalloc [V PEP 703]. +- **단일 스레드 오버헤드.** 3.14 whatsnew: free-threaded 의 단일 스레드 페널티 약 5 ~ 10% [V https://docs.python.org/3.14/whatsnew/3.14.html]. PEP 703 은 6%(Skylake) ~ 5%(Zen 3). +- **소유권 추론이 free-threaded 에서 아끼는 것.** + - 공유 객체(스레드를 건너는 객체)를 borrowed 파라미터로 받으면 원자 연산 쌍(14 ns) 전체를 없앤다. 가장 큰 이득이 나는 자리다. [I, 측정 14.24 vs 3.19 에 근거] + - 스레드 로컬 객체는 쌍이 3.2 ns 라 이득이 GIL 빌드와 비슷하다. + - 불멸 객체(None, 작은 정수, 인턴 문자열)는 이미 0.6 ns. +- **중요한 위험.** 현재 설계의 borrowed 읽기 근거는 "컴파일된 코드는 GIL 을 놓지 않으므로 미지 코드 호출이 없으면 슬롯이 안 바뀐다" [R 4.3.4 나]. free-threaded + 에서는 GIL 이 없으므로 다른 스레드가 `node.left = None` 을 **동시에** 실행해 borrowed 로 쥔 자식을 해제할 수 있다 (use-after-free). 따라서 3.15t 에서 + (a) 호출자가 쥐고 있는 파라미터 자체는 여전히 borrowed 가능, (b) 공유될 수 있는 객체의 **필드 읽기**는 실제 참조 증가(소유 스레드면 3.2 ns)나 임계구역이 필요하고, + (c) borrowed 필드 읽기는 "탈출하지 않은 스레드 로컬 객체"(가상 객체, 영역 객체, 이 호출에서 만들고 아직 저장하지 않은 객체)에만 허용된다. [I] + 같은 이유로 N-12 가상 객체와 영역은 free-threaded 에서도 안전하지만(스레드 로컬이라) 현재 문서의 "다른 스레드는 GIL 때문에 그 사이를 보지 못한다" 문장은 3.15t 에서 거짓이다. + 문서 4.3.4 나 의 마지막 문단("free-threaded 빌드는 범위 밖이다")과 이번 제약("3.15t 전용")이 충돌하므로 갱신이 필요하다. + +## Part B. 메모리: CPython 대 Rust + +### B.1 측정표 + +모든 수치는 요소(또는 객체) 하나당 바이트. "요청" 은 tracemalloc 이 본 파이썬 할당기 요청 바이트에서 리스트 슬롯 8 B 를 뺀 값, "RSS" 는 100만 개(dict 는 10만 개)를 만든 뒤 +maxrss 증가를 개수로 나눈 값으로 **리스트 슬롯 8 B 포함** 이다 (측정 방식이 달라 같은 줄이어도 다른 열은 직접 비교하지 않는다). 소스: measure.out, rssdelta.out. + +| 항목 | getsizeof 3.14 GIL / 3.14t | 요청 3.14 / 3.14t / 3.15t | RSS 3.14 / 3.14t / 3.15t | Rust | +|---|---|---|---|---| +| int (고유, 30비트 초과) | 28 / 44 | 32 / 48 / 48 | 42.4 / 64.2 / 64.8 (리스트 `[int]` 원소당) | i64 = 8 | +| float | 24 / 40 | 24 / 40 / 40 | 42.4 / 64.2 / 64.7 (리스트 `[float]` 원소당) | f64 = 8 | +| `object()` | 16 / 32 | 16 / 32 / 32 | | `()` 0, `Box` 힙 최소 16 (할당기 단위, 측정 안 함) | +| `__slots__` 2 float 필드 클래스 (필드 값 float 제외) | 48 / 48 | 48 / 48 / 48 | float 2개 포함 122.6 / 157.5 / 157.3 | struct{f64,f64} = 16 (Vec 안에 인라인), `Vec>` 원소당 24 [V rs/m] | +| 일반 클래스 2 필드 (필드 값 제외, 인라인 values) | 48 (+dict 288 가상) / 48 | 88 / 88 / 88 | float 2개 포함 170.9 / 204.2 / 203.9 | 16 | +| `list` 100만 int, 원소당 | getsizeof 8.4 (포인터만) | 40.4 / 56.4 / 56.4 (슬롯 포함) | | `Vec` 8 [V rs/m] | +| dict 10만 `str -> int`, 항목당 | getsizeof 38.4 (테이블만) | 120.5 / 152.5 / 152.5 | 171.2 / 170.4 / 171.2 | `HashMap` 55.3 요청 바이트 [V rs/m] | +| 유휴 인터프리터 `python -c pass` maxrss | | | 16.3 MiB (16656 KiB) / 17.3 MiB (17744) / 3.15: 16.6 MiB (16976) / 3.15t: 18.0 MiB (18400) | 빈 `main` 1.4 MiB (1440 KiB) | +| 같은 명령의 peak memory footprint | | | 6.3 MiB / 7.8 MiB / 6.4 MiB / 8.2 MiB | 측정 안 함 | + +- **GC 헤더 포함 여부.** getsizeof 는 GIL 빌드에서 GC 헤더 16 B 를 더해 보고한다: 슬롯 클래스 48 = 헤더 16 + GC 헤더 16 + 슬롯 16, 같은 레이아웃의 non-GC 타입은 32 [V cext/t.out]. + free-threaded 는 둘 다 48. +- **Rust 쪽 수치 출처.** 위 Rust 값은 규칙 계산이 아니라 `rs/m.rs` 를 `rustc -O` 로 컴파일해 전역 할당기로 요청 바이트를 센 값이다 [V `rs/m` 실행]. + `HashMap` 의 capacity 는 114688 로 출력되었다. hashbrown 은 버킷당 제어 바이트 1 B 를 쓰고 적재율이 7/8 이므로 131072 버킷 x (String 24 + i64 8 + 1) 약 4.3 MB 이고, + 키 문자열 9 B 가 따로 힙에 있어 항목당 약 55 B 가 나온다 [V README 줄 35 "only 1 byte of overhead per entry instead of 8" https://github.com/rust-lang/hashbrown; 계산은 [I]]. + 실제 RSS 는 힙 할당기 단위(16 B)로 올림되어 이보다 크다 (측정 안 함). +- **배율(위 표에서 계산).** float 리스트 원소: GIL 약 5x (40/8), 3.15t 약 7 ~ 8x (56 ~ 64/8). 두 float 필드 객체: RSS 기준 GIL 7.7x (122.6/16), 3.15t 9.8x (157.3/16); + Rust 가 박싱해도(24) 5 ~ 6.6x. dict 항목: GIL 2.2x (120.5/55.3), 3.15t 2.8x (152.5/55.3). 유휴: 약 12x (16.3/1.4 MiB). +- **측정의 한계.** RSS 증가는 `ru_maxrss` 기반이라 단조 증가 값이다. 측정은 한 번에 하나씩 했고 로드 평균은 약 2 였다. 분산(반복)은 유휴 RSS 3회만 기록했다(거의 동일). + `-I -S -OO` 실행에서 3.14 가 기준보다 큰 18016 KiB 였던 것은 최적화 레벨 2 의 pyc 가 없어 소스를 컴파일했기 때문일 것으로 [I] 보며 확인하지 않았다. + +### B.2 현재 설계가 줄이는 것과 줄이지 못하는 것 + +| 현재 설계 요소 | 줄이는 것 | 줄이지 못하는 것 | +|---|---|---| +| 가상 객체 + 영역 (N-12) | 컴파일 구간 안에서만 사는 임시 객체의 할당과 해제 전체 (객체 하나당 48 B 와 할당 비용) | 컨테이너·전역·필드에 저장되는 장기 객체, 인터프리터로 넘어가는 객체 (실체화되면 위 표 그대로) | +| unbox 된 i64/f64 지역 변수 | 지역 스칼라의 박싱 (float 한 개 32 B, 3.15t 40 ~ 48 B) | 슬롯, 리스트, dict 에 저장되는 값은 저장 시점에 박싱 | +| `list[float]`/`list[int]` 네이티브 배열 | 컴파일 구간 안의 복사본 | 인터프리터가 쥔 `list` 자체 (원소당 40 ~ 56 B) | +| N-11 고정 레이아웃 | dict 288 B 가상 크기와 인라인 values 40 B 를 슬롯 16 B 로 | 슬롯 값은 여전히 `PyObject*` 라서 float 필드마다 박싱 32 ~ 48 B 가 따로 든다 | +| 소유권/refcount 생략 | 시간 | 크기 | +| 인터프리터 기본 비용 | 없음 | 16 ~ 18 MiB 유휴 RSS (Rust 1.4 MiB) | + +### B.3 대안 표 (장기 메모리를 줄이면서 동일성 유지) + +절약 추정은 모두 위 표의 숫자에서 계산한 값이며 구현해 보지 않았다. + +| 번호 | 대안 | 절약 추정 (객체당, 3.15t / GIL) | 관측 위험 (동일성 규칙 대비) | 필요한 사용자 결정 | +|---|---|---|---|---| +| O1 | N-11 슬롯의 float/i64 unbox (엄격형). `Py_T_DOUBLE` 멤버 | 두 float 필드 객체 157 B 에서 48 B (3.3x) / 122.6 에서 40 B (3.1x) | **관측됨**: `a.x is a.x` False, `id(a.x)` 매 읽기마다 달라짐 [V cext/t.out DblObj]; `a.x = 1` 은 int 가 float 으로 바뀜(`type(a.x)`), 문자열 대입은 `TypeError`; i64 슬롯은 큰 정수·bool 을 못 담음; 타입이 컴파일러 생성 (아래 O5) | D1, D2, D3 | +| O2 | 같은 슬롯을 NaN-boxing 으로 (double 또는 `PyObject*` 태그). 비 float 값은 포인터로 저장 | O1 과 같은 크기 (슬롯은 8 B). GC 타입 유지 시 GIL 은 40 에서 56 B | `is`/`id` 만 관측됨 (float 읽기마다 새 객체). 타입 보존·임의 대입 가능. 단 `tp_traverse` 가 태그를 구분해야 하고 PyPy 의 방식과 같다 [I]. 3.15t 에서 매 읽기마다 새 float 할당(40 B 단명, 민감 경로에서 느려질 수 있음, 측정 안 함) | D1, D2 | +| O3 | N-11 클래스를 non-GC 로 (O1 과 결합할 때만 건전) | GIL 16 B (33%) / 3.15t 0 B [V] | `gc.is_tracked`, `gc.get_objects`, `sys.getsizeof`(GIL) 가 정의상 달라짐 [V]; final 이어야 함 [I]; `PyObject*` 슬롯과 결합하면 순환 누수로 건전성 상실 | D2, D3, D4 | +| O4 | 장기 데이터를 소스에서 `array.array('d')`/numpy 같은 압축 컨테이너로 쓰도록 안내 (엔진 변경 없음) | float 리스트 원소 56 ~ 64 B 에서 8 B (7 ~ 8x) | 엔진은 건드리지 않으므로 없음. 사용자 코드가 타입을 바꾸는 선택 (원소 `is` 동일성도 같이 바뀜) | 정책 결정 (검사기에서 `list[float]` 경고 여부) | +| O5 | `PyType_FromSpec` 로 컴파일러가 N-11 타입을 직접 생성 (O1 ~ O3 의 전제) | 자체로는 0 | `__dict__` 항목, `__flags__`, `__basicsize__`, `__slots__` 속성, 서브클래싱, pickle/copy/dataclass 연동, 인터프리터 폴백 때 쓰는 클래스와의 일치 | D2 | +| O6 | 인터프리터 기반 비용: `-S` (site 생략), frozen stdlib 유지(기본 on), 지연 import | `-S`: 16656 에서 15632 KiB (-1.0 MiB), 모듈 33 개에서 21 개 [V]; `-X frozen_modules=off` 는 21120 KiB (+4.4 MiB) 라서 frozen 을 반드시 켜 둘 것 [V] | 없음 (site 가 하는 일을 앱이 필요로 하지 않을 때). `sys.path` 에 site-packages 가 없어짐 | 임베딩 시 `-S` 허용 여부 | +| O7 | `PYTHONMALLOC=malloc` | 0 (유휴 +0.5 MiB, float/객체 RSS 동일) [V] | 없음 | 불필요 (효과 없음) | +| O8 | free-threaded 의 mimalloc 설정, 3.15t 대신 GIL 빌드 | 3.15t 의 float 40 B / 객체 헤더 32 B 의 추가 비용은 빌드 선택으로만 줄어듦 (GIL 은 float 24, int 28) | 빌드 선택은 제약 "3.15t 전용" 과 충돌 | 제약 변경 여부 (권고하지 않음) | +| O9 | 인터프리터 풋프린트: 유휴 16 ~ 18 MiB 는 libpython 매핑과 초기 모듈이 대부분 | 줄일 수 있는 것은 O6 정도 (-1 MiB), 나머지는 못 줄임 | | 없음 | + +### B.4 권고 + +1. **소유권 추론(A.2 (1))은 구현해도 된다.** 관측 가능한 변화가 없고 사용자 결정이 필요 없다. 단, 3.15t 용 조건(A.2 (4)의 borrowed 필드 읽기 제한)을 설계에 먼저 반영한다. 크기에는 도움이 안 되고 시간에만 도움이 된다고 기대치를 낮춘다. +2. **non-GC 클래스(O3)는 단독으로는 하지 않는다.** 3.15t 에서 메모리 이득이 0 이고 (표 3.15t 열), 건전성이 슬롯 표현에 달려 있다. O1 과 한 묶음일 때만 의미가 있다. +3. **장기 메모리 이득의 본체는 O1/O2 (슬롯 unbox)와 O4 (압축 컨테이너)** 다. O4 는 결정이 거의 필요 없으므로 먼저 문서화·검사기 경고로 시작하고, O1/O2 는 `is`/`id` 결정 후 진행한다. +4. **O1 보다 O2 를 먼저 검토한다.** O2 는 관측 차이를 `is`/`id` 하나로 좁히고 O1 의 타입 강제 문제를 없앤다. 대가는 읽기마다 float 할당과 `tp_traverse` 복잡도다. +5. 가상 객체와 영역(N-12)이 장기 데이터에 안 닿는다는 점을 SPEC N-12 에 한 줄로 못박아 기대 관리를 한다. + +### B.5 필요한 사용자 결정 + +- **D1** float/int 슬롯 값의 객체 동일성(`a.x is a.x`, `id(a.x)`)을 동일성 보증 범위에서 뺄 것인가. 근거 문장: 데이터 모델 3.1 은 불변 타입에서 "새 값을 계산하는 연산"이 기존 객체를 돌려줄 수 있다고만 허용한다 [V https://docs.python.org/3/reference/datamodel.html]. 속성 읽기는 "새 값 계산" 이 아니므로 읽을 때마다 새 float 를 주는 것은 엄밀히는 허용 밖이다. [I] GC 시점 (b) 결정과 같은 방식의 명시적 예외가 필요하다. +- **D2** 컴파일러가 N-11 타입을 `PyType_FromSpec` 로 직접 만들고 클래스 문 결과를 대체해도 되는가 (`__flags__`/`__basicsize__`/`gc` 관측 차이 허용 범위). +- **D3** 슬롯 타입 강제(`o.x = "s"` 가 `TypeError`) 허용 여부. O1/O3 에는 필요하고 O2 에는 불필요하다. +- **D4** non-GC 타입을 final 로 만들어 서브클래싱을 막아도 되는가 (O3 한정). +- **D5** 다른 스레드 동시 접근 가능성이 있는 3.15t 에서 N-12 4.3.4 나 의 borrowed 읽기 근거를 어떻게 바꿀 것인가 (스레드 로컬 증명만 허용 / 실제 incref 유지). 설계 문서 수정 승인. + +## 7. 부록: 직접 측정한 보조 수치 + +- R-1 INCREF+DECREF 쌍(ns) [V cext/rc.out, cext/rc.c, N=5천만, 최소값]: + + | 빌드 | 소유 스레드 객체 | 다른 스레드가 만든 객체 | 불멸(None) | + |---|---|---|---| + | 3.14.7 GIL (인라인) | 3.96 | 3.95 | 0.35 | + | 3.14.7 GIL (limited API 0x030c, 함수 호출) | 4.12 | 4.14 | 2.22 | + | 3.14.7t | 3.21 | 14.20 | 0.63 | + | 3.15.0rc1t | 3.19 | 14.24 | 0.63 | +- R-2 인터프리터 3.14 `-X frozen_modules=off`: 21120 KiB, `-S`: 15632 KiB, `-S -E`: 15632 KiB; 3.14t `-S -E`: 16880 KiB; 3.15t `-S -E`: 17424 KiB [V rss.out]. +- 3.14.0 ~ 3.14.4 의 증분 GC 는 3.14.5 에서 되돌려졌다고 whatsnew 가 말한다 [V https://docs.python.org/3.14/whatsnew/3.14.html]. 이 연구는 3.14.7 과 3.15.0rc1 에서 했다. + +## 8. 이 방법이 찾지 못하는 것 (Part C 포함) + +단일 머신 마이크로벤치와 합성 클래스(float 2개)로 구조 크기만 쟀으므로, 실제 앱의 객체 구성·할당기 단편화·멀티스레드 경합 아래의 실효 절약과 (O2 의 읽기당 새 float 할당 같은) 시간 비용, 3.15t + abi3t 로 컴파일한 확장의 실제 refcount 호출 비용은 찾지 못한다. + +## Part C. 인터프리터는 갱신 가능한 코드에만, 그 밖에는 최소 풋프린트 + +원자료: rssC.out, impcost.py, release.py, rel2.py, lazy.py/eager.py. 측정은 모두 같은 머신, 한 번에 하나씩, 3.14.7 / 3.14.7t / 3.15.0rc1 / 3.15.0rc1t. +주의: 새 설치 직후 첫 실행은 stdlib pyc 를 쓰느라 RSS 가 +8 ~ 9 MiB 크게 나온다 (3.14t 에서 -S -I +imports 가 38144 KiB 로 나왔다가 재측정에서 29280 KiB). 아래 표는 pyc 가 만들어진 뒤 정상 상태의 값이다. + +### C.1 유휴와 소규모 앱의 RSS (maxrss KiB / peak memory footprint KiB) [V rssC.out] + +| 빌드 | `-c pass` | `-S -I -c pass` | `-S -I` + json,re,dataclasses,typing,asyncio | site 포함 + 같은 import | `-S -I -OO` + 같은 import | +|---|---|---|---|---|---| +| 3.14 GIL | 16640 / 6433 | 15632 / 5697 | 26160 / 14785 | 26624 / 15249 | 25808 / 14449 | +| 3.14t | 17744 / 7953 | 16880 / 7329 | 29392 / 18577 | 29472 / 18641 | 28576 / 17745 | +| 3.15 GIL | 16864 / 6449 | 15904 / 5793 | 27248 / 15649 | 27280 / 15649 | 26736 / 15137 | +| 3.15t | 18416 / 8353 | 17424 / 7617 | 30384 / 19201 | 30720 / 19505 | 29712 / 18529 | + +- import 5개의 증가분: GIL 3.14 +10.5 MiB, 3.14t +12.5 MiB, 모듈 21개에서 147개(126개 추가). json 하나만: +2.3 MiB (GIL), re+typing+dataclasses: +5.1 MiB. [V] +- Rust 빈 `main` 은 1.4 MiB (Part B). `-S -I` 유휴는 그것의 약 11 ~ 12배다. [V] + +### C.2 바닥(floor)을 이루는 것 + +| 구성 | 크기 | 근거 | +|---|---|---| +| 파이썬 힙(pymalloc)에 살아 있는 객체 | `-S -I` 유휴에 1,022,416 B, 블록 12,370 개, 1 MiB 아레나 2개(사용 가능 블록 261,808 B). GC 추적 객체 4,484 개, 대부분 `wrapper_descriptor` 1094, `method_descriptor` 797, `builtin_function_or_method` 694, `function` 672, `dict` 617 (내장 타입과 모듈의 메서드 표) | [V `sys._debugmallocstats`, `gc.get_objects`] | +| 물리 풋프린트(private/dirty) | GIL 5.7 MiB, 3.14t 7.3 MiB (`-S -I`). RSS 와의 차이 약 10 MiB 는 libpython(18.8 MB 파일)과 실행 파일·공유 캐시의 clean 파일 매핑 페이지다. 앱마다 중복되지 않는 공유 가능 페이지라서 "앱이 더하는 비용" 은 풋프린트 쪽이 맞다 | [V rssC.out, vmmap Physical footprint 8.6M(subprocess 를 import 한 측정이라 위 표보다 큼)]. 차이의 원인 서술은 [I] | +| 힙 밖 C 할당 | vmmap 상 MALLOC_SMALL+TINY 이 GIL 에서 약 2.7 MiB dirty (인터프리터 상태, 인턴 테이블 등으로 추정), 3.14t 에서는 0.6 MiB 이고 대신 풋프린트 합이 커짐 (mimalloc 이 VM 영역을 직접 씀) | [V vmmap -summary], 용도 추정은 [I] | +| free-threaded 추가분 | 유휴 +1.1 MiB RSS, +1.6 MiB 풋프린트. 객체 헤더 32 B 와 float 40 B 등 (Part B), 스레드별 mimalloc 힙 | [V], 스레드별 힙의 몫은 분리 측정하지 못함 [I] | +| import 한 모듈의 사전·코드 객체 | 5개 import 후 tracemalloc 으로 6.0 MiB (GIL) / 7.1 MiB (FT) 가 파이썬 할당기에 남음. 이 중 코드 객체 4,013 개의 `getsizeof` 합 1.67 MiB, 그 중 line table + exception table 0.69 MiB (코드 객체 크기의 41%). 나머지는 모듈 사전, 함수·클래스 객체, 상수, 확장 모듈(`_json`, `_asyncio` 등)의 C 상태 | [V impcost.py]. 나머지 분해는 [I] | + +결론 [I]: 유휴 바닥(약 5.7 MiB 풋프린트)은 CPython 런타임이 정하고 앱이 줄일 수 있는 부분이 거의 없다. 앱이 영향을 줄 수 있는 쪽은 **무엇을 import 하느냐**(최대 +10 ~ 12 MiB)다. + +### C.3 항목별 평가 + +| 항목 | 절약 추정 | 동일성 위험 | 노력 | 검증 상태 | +|---|---|---|---|---| +| 임베딩 초기화 최소화: `PyConfig_InitIsolatedConfig` + `site_import=0` + 명시적 `module_search_paths` | `-S`: RSS -1.0 MiB (16640 → 15632 KiB), 풋프린트 -0.7 MiB, 모듈 33 → 21 [V]. 격리 설정 자체는 환경 변수, argv, 사용자 site 를 무시 [V https://docs.python.org/3.14/c-api/init_config.html] | `sys.path` 에 site-packages 가 없다. `quit`/`exit`/`help` 내장이 없다. `sys.flags` 가 달라진다. 의존 패키지 경로를 우리가 구성해야 한다 (이미 Gradle 플러그인이 PYTHONHOME 을 스테이징하므로 자연스러움) | 낮음 | 절약 [V 측정], 위험 [I]. 같은 페이지에서 가져온 "최소 구성 예제" 코드는 `PyConfig` 와 `PyInitConfig_SetInt` 를 섞어 쓰는 것으로 읽혀 요약기의 합성일 수 있어 사용하지 않았다 | +| frozen stdlib / 최소 stdlib | 기본값 `frozen_modules` 켬이 이미 이득: 끄면 +4.4 MiB (16656 → 21120 KiB, 앞 측정). 추가 이득은 "안 쓰는 모듈 제외" 인데 RSS 는 import 한 만큼만 늘므로 디스크는 줄어도 RSS 는 import 안 한 모듈에서는 안 줄어든다 | import 하지 않을 모듈을 삭제하면 `ImportError` 가 달라진다. 닫힌 세계 증명이 있어야 함 | 중간 (이미 아티팩트 워커가 의존 그래프를 안다) | 켬/끔 차이 [V], 삭제의 RSS 이득이 거의 없다는 것은 [I] | +| PEP 810 명시적 lazy import | 3.15t 에서 다섯 모듈을 쓰지 않을 때 30272 → 17536 KiB (-12.7 MiB, -42%), 3.15 GIL 27328 → 16000 KiB. json 만 실제로 쓰면 20528 KiB [V lazy.py 등]. 3.15 에 실제로 들어갔다 [V PEP 810 상태 Final, Python-Version 3.15, 결정일 2025-11-03 https://peps.python.org/pep-0810/ ; whatsnew 3.15 가 `lazy import json` 와 `-X lazy_imports`, `PYTHON_LAZY_IMPORTS`, `sys.set_lazy_imports()` 를 서술 https://docs.python.org/3.15/whatsnew/3.15.html] | 사용자가 `lazy` 를 쓴 곳은 사용자 선택이라 위험 없음. 컴파일러가 임의로 lazy 로 바꾸면 import 부작용의 순서와 `ImportError` 발생 시점이 달라져 관측됨. 전역 모드(`-X lazy_imports`)는 같은 이유로 권하지 않음 | 낮음 (사용자 코드), 높음 (자동 변환) | 절약·상태 [V], 위험 [I]. 3.14 에는 없다 (우리는 3.15t 전용이라 무관) | +| docstring 제거(-OO), 줄 표 제거 | `-OO`: RSS -0.35 MiB (GIL), -0.8 MiB (3.14t) [V]. `-X no_debug_ranges` 를 더해도 RSS 변화가 관측되지 않았다 (26144 vs 25808 KiB, 3.14t 28848 vs 28576, 잡음 범위) [V]. line+exception tables 합 0.69 MiB 가 상한 | `-OO` 는 docstring 뿐 아니라 `assert` 문도 제거한다 (의미 변경, 관측됨). `__doc__` 이 None 이 되어 `dataclasses`, `argparse`, 일부 라이브러리가 달라진다. 줄 표를 지우면 traceback 의 줄 번호가 사라진다 (관측됨) | 낮음 | 절약 [V 소규모], 위험 [I]. 권고하지 않음 (이득이 작고 위험이 큼) | +| `gc.freeze` | 단일 프로세스 RSS 이득 없음. 목적은 `fork()` 후 copy-on-write 공유와 GC 순회 감소다 [V https://docs.python.org/3.14/library/gc.html]. free-threaded GC 는 메모리가 10% 늘지 않으면 수집을 건너뛴다 [V 같은 페이지] | `gc.get_freeze_count()`, `gc.get_objects()` 에 영향. 앱이 안 쓰면 없음 | 낮음 | [V 문서], 이득 없음 판단은 [I] | +| 해제한 메모리를 OS 에 반환 | **GIL (pymalloc)**: 100만 객체 해제 후 풋프린트 125.8M → 20.7M 로 돌아옴 (빈 아레나 해제) [V rel2.py]. 단 1% 만 흩어 남기면 아레나가 묶여 RSS 144 MiB 가 그대로 [V release.py]. **free-threaded (mimalloc)**: 같은 시험에서 158.7M → 158.9M, 즉 **반환되지 않음**. `MIMALLOC_PURGE_DELAY=0` 을 줘도 같음 (환경 변수를 이 빌드가 읽는지는 확인 못함) [V rel2.py, release.py]. libpython3.14t 는 `mi_collect` 류 심볼을 내보내지 않는다 [V nm -gU]. CPython 은 mimalloc 힙 수집을 QSBR 판정용 내부 경로에서만 부른다 [R src/obmalloc.c 줄 224] | 없음 (할당기 동작은 파이썬 의미가 아님) | FT: 높음. 직접 빌드한 CPython 에서 `mi_collect(true)` 를 노출하거나 `purge_delay` 를 설정해야 한다 (PBS 배포 바이너리로는 불가) | 측정 [V], 원인과 해법은 [I] | +| 컴파일 모듈이 deopt 용으로 들고 있는 소스·바이트코드의 지연 로드 | 코드 객체 평균 약 425 B (1.67 MiB / 4013), 그 41% 가 위치·예외 표 [V]. 컴파일 함수 1000 개면 약 0.4 MiB 로 바닥 대비 작다 [I]. 모듈 본문은 import 때 한 번 실행되고 그 코드 객체는 보관되지 않으므로 줄일 수 있는 것은 함수 코드 객체뿐이다 [I] | deopt 첫 호출이 느려진다. `__typedpython_interpreted__` 의 함수를 지연 생성 객체로 바꾸면 그것을 검사하는 코드가 다르게 본다 (드묾). FT 에서 지연 로드는 동기화가 필요하다 | 중간 | 절약 [I], 크기 통계 [V] | +| FastTrack: 갱신된 모듈은 인터프리터로, 건드리지 않은 모듈은 컴파일 상태로 | 모듈 단위는 N-10 증분 단위와 같아 구조적으로 맞는다 [R 설계 문서 4.3.2]. 그러나 컴파일 모듈이 소스를 먼저 실행해 네임스페이스를 만든다는 현재 설계 [R 4.3 "모듈 모양"] 때문에 컴파일된 모듈도 인터프리터와 소스(또는 pyc)를 import 때 쓴다. 인터프리터 자체는 컴파일 확장이 C API 와 파이썬 힙 위에 산다는 제약상 제거할 수 없다. 즉 바닥 5.7 MiB 풋프린트는 이 아이디어로 줄지 않는다 [I]. 줄일 수 있는 것은 "건드리지 않은 모듈의 인터프리터용 소스·코드 객체를 아예 안 싣는 것" 인데, 그러려면 컴파일러가 클래스·함수 정의를 C 초기화 코드로 내보내야 한다 | 클래스 생성 의미(메타클래스, `__init_subclass__`, 데코레이터 순서)를 C 로 재현해야 하므로 위험이 크다. 갱신 모듈과 컴파일 모듈 사이의 가정은 N-11 의 `tp_version_tag` 와 전역 바인딩 확인이 이미 일반 경로로 떨어뜨린다 [R 4.3.3] | 높음 | [I] 전부. 현재 코드가 실제로 그렇게 동작하는지는 확인하지 않았다 | + +### C.4 권고 (Part C) + +1. **먼저 할 것(위험 없음, 낮은 노력)**: 격리 구성과 `site_import=0` (약 1 MiB), `frozen_modules` 유지, 사용자 코드에서 `lazy import` 안내와 검사기 경고 (큰 라이브러리 import 가 12 MiB 까지 좌우). +2. **하지 않을 것**: `-OO` (assert 제거), 줄 표 제거, 컴파일러 자동 lazy 변환, `gc.freeze` (이득이 작거나 의미가 바뀐다). +3. **3.15t 에서 가장 큰 구조적 문제는 메모리 반환**이다. 피크 후 해제한 메모리가 돌아오지 않는다 (158.9M 유지). 장시간 실행 앱에는 피크 = 상주다. CPython 을 직접 빌드해 mimalloc 수집을 노출할 수 있는지가 열린 질문이다. +4. "인터프리터는 갱신 코드에만" 은 현재 제약(컴파일 확장이 CPython 힙 위)에서는 **풋프린트 바닥을 줄이지 못한다**. 얻는 것은 모듈 단위의 코드 객체와 소스 지연 로드 정도(수 MiB 이하)다. + +### C.5 이 절의 사용자 결정 + +- **D6** 임베딩에서 `site` 를 끄고 `module_search_paths` 를 우리가 구성해도 되는가 (`quit`/`exit`/`help` 내장 부재 허용). +- **D7** 3.15t 용 CPython 을 직접 빌드(또는 패치)해 mimalloc 반환을 제어해도 되는가 (PBS 바이너리 의존 탈피). +- **D8** FastTrack 에서 컴파일 모듈의 인터프리터용 정의를 제거하는 설계(클래스·함수를 C 초기화로 내보냄)를 진행할 가치가 있는지. diff --git a/docs/investigations/typedpython-vs-cython-numba-lpython.md b/docs/investigations/typedpython-vs-cython-numba-lpython.md new file mode 100644 index 000000000..5851dc555 --- /dev/null +++ b/docs/investigations/typedpython-vs-cython-numba-lpython.md @@ -0,0 +1,159 @@ +# TypedPython against Cython, numba and LPython (design-level comparison) + +Requested by the user (2026-10-04, via the leader): compare the current TypedPython design with +Cython, numba and LPython **logically first**; measurements come later (the harness is on +`feat/bench-cython-numba`, not yet measured). Issues: #41, #125. + +Evidence tags: **[doc]** stated in the tool's own documentation (links at the end), **[src]** read in +this repository's code, **[measured]** a number from our own runs (2026-10-03 quiet window, develop +e888e7f5, macOS arm64), **[inferred]** reasoning, not verified. + +## Conclusion + +**Where TypedPython wins** + +1. **Deployment.** It is ahead-of-time C that becomes a CPython extension, built with the target's own C compiler. So it runs on iOS, on Android, and next to a GraalVM native image. [src] [doc] + - numba compiles at run time through LLVM, and its AOT path (`numba.pycc`) is deprecated with no replacement yet [doc]. It needs LLVM on the device, which rules out iOS. [inferred] + - LPython AOT produces standalone binaries, and its `@lpython` JIT needs a C compiler at run time [doc]. Neither fits an app that embeds CPython. [inferred] + - Cython has the same deployment model as TypedPython. [doc] +2. **CPython identity by default.** The others give up semantics either by default or once you type the code. + - What TypedPython keeps: + - int overflow promotes to a big int, via deopt [src]; + - bounds checks stay unless proven [src]; + - exceptions are CPython's own [src]; + - signals and GIL hand-off work since #141 phase 1 [src]. + - numba: integers are fixed width and wrap; bounds checks are off by default; globals are frozen at compile time. [doc] + - Cython: typed C integers wrap unless `overflowcheck=True` (off by default). [doc] + - LPython: requires its own fixed-width types (`i32`, `f64`). [doc] +3. **No new syntax, gradual by function.** + - TypedPython takes types from ordinary annotations plus Pyrefly's inference. A function it cannot prove stays interpreted, with a reason. [src] + - Cython needs `cdef` or `cython.*` types to leave the object path. [doc] + - LPython needs its own type names. [doc] + - numba needs no annotations, but rejects code outside its subset with a `TypingError`. [doc] +4. **Kotlin interop inside compiled code.** No other tool has a counterpart. [src] + +**Where TypedPython loses, and the design item that would close each gap** + +| Gap | Who wins it | Closing design item | Identity cost | +|---|---|---|---| +| Per-call overhead on small leaf functions (spectral-norm's `eval_a`): `tp_enter_call`/`tp_leave_call` on every call, probably no inlining | numba and Cython inline small functions [inferred] | Depth precheck plus a fast entry for callees outside every cycle, with a fallback to the per-call path (#41 plan), then let the C compiler inline | none | +| Checked i64 arithmetic in hot integer code | numba, Cython typed, LPython (unchecked) | Interval proofs that drop provably safe checks (for example `i + j` with both below `len(list)`), and a shift for a floor division by a power of two of a proven non-negative value | none | +| Lists are copied in and written back, O(n) per call | numba (numpy arrays) and Cython (memoryviews) are zero-copy [doc] | Accept buffer-protocol arrays (`array.array`, numpy, memoryview) as zero-copy `ArrayParam`. Since no user code runs while the array is live (the existing contract), direct access is observably the same | none | +| Strings and dicts (wordfreq is 1.1x [measured]) | numba `typed.Dict` [doc], Cython C-level code [inferred] | An exact-type `dict[str, int]` and `str` fast path that calls the same C API (`PyDict_GetItem`/`SetItem` on exact `dict` and exact `str` keys, so no user `__hash__` or `__eq__` runs) | none | +| Object allocation (binary-trees is 1.7x [measured]) | Cython `@cython.freelist` [doc]; numba keeps objects out of CPython's GC [doc] | N-12 virtual objects (#143) | **GC timing**, a user decision | +| Float reductions are not vectorised | numba `fastmath=True`, Cython `-ffast-math` (both opt-in) [doc] | None that keeps identity: reassociating float sums changes results. Integer reductions and element-wise loops still vectorise. An opt-in `fastmath` would break identity, so it is a user decision | **yes**, if offered | +| Parallel loops | numba `parallel=True`/`prange`, Cython `prange` with OpenMP [doc] | After free-threaded support: parallel loops whose result is order-independent (element-wise), or reductions in a fixed order | none if restricted that way | + +Defaults are on the same footing: numba's `fastmath`, `parallel` and `nogil` are all off by default +[doc], and Cython's default directives keep Python semantics (`boundscheck=True`, `wraparound=True`, +`cdivision=False`) [doc]. Most of the "they are faster" headline numbers come from opting out of +semantics. + +## 1. Where type information comes from, and what happens when it is missing + +| | TypedPython | Cython | numba | LPython | +|---|---|---|---|---| +| Source | Ordinary annotations plus Pyrefly's static inference [src] | `cdef`/`cython.*` declarations; in pure mode, `float` becomes a C double while `int` stays a Python object [doc] | Specialised at run time from argument types, with inference over the bytecode [doc] | Mandatory annotations in its own types (`i32`, `f64`, `list[i32]`) [doc] | +| When it cannot type something | The function stays interpreted, with a diagnostic; a guard failure at run time deopts that one call [src] | Compiles anyway on the generic object path (slow, but correct) [doc] | `TypingError`: the function does not compile [doc] | Compile error [doc] | +| Granularity | Per function, and per call when deopting [src] | Per variable | Per function, per argument signature | Per program | + +## 2. Code generation path and when compilation happens + +| | Path | When | Mobile / GraalVM-embedded CPython | +|---|---|---|---| +| TypedPython | Typed IR, then C, then the target's C compiler, giving a CPython extension module [src] | AOT, incremental per module [src] | Yes: NDK, Xcode, desktop clang [src] | +| Cython | C, then a C compiler, giving an extension module [doc] | AOT | Yes (Kivy and BeeWare ship Cython extensions) [inferred] | +| numba | LLVM IR, JIT through llvmlite [doc] | At the first call per signature; `cache=True` stores it on disk [doc]; `numba.pycc` AOT is deprecated [doc] | No on iOS (JIT, LLVM on the device) [inferred] | +| LPython | ASR, then LLVM, C, C++, WASM or x86 [doc] | AOT standalone binaries, or the `@lpython` JIT (C backend only at present) [doc] | Not as part of an embedded CPython [inferred] | + +## 3. Object model + +- **TypedPython** [src] + - Unboxing: scalars (`int` as i64, `float` as double, `bool`) are unboxed. + - Arrays: `list[float]` and `list[int]` parameters become native arrays through copy-in and write-back, and local arrays are native. + - Classes: `__slots__` classes (N-11) are real objects with direct slot access. + - Everything else is an opaque object. + - Refcounting: every object operation does a real incref or decref. + - Not covered: no dict or str fast path. +- **Cython** + - Unboxing: C types where declared. [doc] + - Classes: `cdef class` objects are real CPython objects with C fields. [doc] + - Arrays: memoryviews give zero-copy access to buffers. [doc] + - Refcounting: Cython's own code generation does it. [doc] + - Lists and dicts go through the C API. [inferred] +- **numba** + - Its data lives outside CPython: arrays are managed by NRT, and `typed.List` and `typed.Dict` are numba's own types. [doc] + - Classes: `jitclass` is experimental. [doc] + - Lists: reflected Python lists are pending deprecation. [doc] + - Interop: Python objects appear only at the boundary or in `objmode`. [doc] +- **LPython** + - Its own value types, with lists and dicts of fixed element types. [doc] + - Interop: CPython objects are reached through `@pythoncall`. [doc] + +## 4. CPython identity + +| | TypedPython | Cython (typed) | numba | LPython | +|---|---|---|---|---| +| Integer overflow | Promotes to a big int (deopt) [src] | Wraps; `overflowcheck` is off by default [doc] | Fixed width, wraps [doc] | Fixed width (`i32`/`i64`) [doc] | +| Bounds | Checked unless proven [src] | `boundscheck=True` by default [doc] | Off by default [doc] | Not documented [doc gap] | +| Division by zero | CPython's `ZeroDivisionError` [src] | `cdivision=False` by default [doc] | `error_model='python'` by default [doc] | Not documented | +| Globals | Read live, or through an entry snapshot plus a guard (closed functions) [src] | Live | **Frozen at compile time** [doc] | Not applicable | +| Signals, other threads | Polled since #141 phase 1; deferred while a snapshot is live [src] | Not polled unless the code calls `PyErr_CheckSignals` [inferred] | Not polled; `nogil=True` releases the GIL [doc] | Not applicable | +| Trace and profile | No events (#142) [src] | `linetrace`/`profile` directives, off by default [doc] | None [inferred] | Not applicable | +| GC timing | Unchanged for real objects; N-12 would change it [src] | Unchanged [inferred] | Its data is not GC-tracked [inferred] | Not applicable | +| Exceptions | CPython's own; arrays write back partial updates [src] | CPython's own | Limited (constant arguments); memory allocated in a function that raises leaks [doc] | Not documented | + +## 5. Loops and arrays + +- Bounds-check elimination: + - TypedPython uses interval proofs that the verifier checks again (`proven` ops). [src] + - Cython and numba drop all bounds checks when told to (`boundscheck=False` or the default). [doc] +- Vectorisation: + - All four leave it to the backend (clang or LLVM). [inferred] + - TypedPython builds with `-ffp-contract=off -fno-fast-math` for bit-identical floats, so float reductions do not vectorise, and neither do they under numba's or Cython's defaults. [src] [inferred] +- Parallelism: + - numba and Cython have `prange`. [doc] + - TypedPython has none yet; it needs the free-threaded build first (AGENTS.md §12.8). + +## 6. Call boundary cost + +- **Python → compiled code** + - TypedPython: a vectorcall wrapper checks argument types exactly and copies arrays in. The copy is O(n) per call, which is a real cost for many small calls on large lists. [src] + - Cython: a C call, plus unboxing, plus buffer acquisition for memoryviews. [inferred] + - numba: a dispatcher that typechecks the arguments against the compiled signatures. [doc] [inferred] +- **compiled code → Python** + - TypedPython and Cython: a C API vectorcall. [src] [inferred] + - numba: only through `objmode` blocks. [doc] + - LPython: through `@pythoncall`. [doc] +- **compiled → compiled** + - TypedPython: a direct C call, but with depth counting on every call. That is the spectral-norm finding. [src] + - Cython: a direct C call. [inferred] + - numba: calls between jitted functions inline through LLVM. [inferred] + +## 7. The three numeric benchmarks: theoretical ceilings + +The measured numbers are interleaved medians from the 2026-10-03 quiet window, speedup against CPython 3.13 with compiled times. [measured] + +| benchmark | TypedPython now | Rust | Node | Ceiling for numba, Cython typed and LPython [inferred] | What TypedPython needs to reach it | +|---|---|---|---|---|---| +| nbody | 31.3x (0.129 s) | 0.059 s | 0.117 s | About C speed, near Rust. Short arrays and float math, no reductions across bodies | Find the remaining 2x to Rust first. Candidates: bounds checks not removed on `pos[i]`-style indexing, no inlining across helper calls, and the call per step. Needs the #125-style profile; not verified | +| spectral-norm | 6.3x (0.518 s) | 0.027 s | 0.068 s | About C speed: `eval_a` inlined, integer arithmetic unchecked, the floor division a shift | (1) the depth precheck and fast entry; (2) inlining of `eval_a`; (3) the interval proof `i + j < 2n`, plus a shift for `// 2` once the value is proven non-negative. The `(i+j)*(i+j+1)` product cannot be proven safe without a bound on `n`, so its check stays, but a predicted branch costs little | +| fannkuch-redux | 23.5x (0.250 s) | 0.249 s | 0.261 s | About C speed | Already at Rust parity [measured]; nothing needed | + +## LPython: development state (checked 2026-10-04) + +- The README says "currently in alpha stage and under heavy development". [doc] +- Latest release: v0.22.0 (2024-07-08). Last push to `main`: 2025-12-11. 580 open issues. [src: GitHub API] +- It compiles "a subset of Python" with mandatory LPython type annotations. [doc] +- Backends: LLVM, C, C++, WASM, Julia, x86. [doc] +- The `@lpython` JIT supports only the C backend at present. [doc] +- It reaches CPython libraries through `@pythoncall`. [doc] + +Assessment [inferred]: the project is active but slow, a long way from a stable 1.0. It is not built to run inside an embedded CPython, so it is not a candidate backend for this repository. The design doc's earlier note (§4.3.3 "upstream activity moved to LFortran") still matches the release history: no release since 2024-07. + +## Sources + +- numba: [JIT options](https://numba.readthedocs.io/en/stable/reference/jit-compilation.html), [deviations from Python semantics](https://numba.readthedocs.io/en/stable/reference/pysemantics.html), [deprecation notices](https://numba.readthedocs.io/en/stable/reference/deprecation.html) +- Cython: [compiler directives](https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html) +- LPython: [repository README](https://github.com/lcompilers/lpython), [2023 announcement](https://lpython.org/blog/2023/07/lpython-novel-fast-retargetable-python-compiler/), GitHub API for release and push dates +- TypedPython: `python-multiplatform-ksp/src/main/python/typedpython/` (`ir.py`, `cgen.py`, `runtime/tp_runtime.h`, `cbuild.py`), `docs/design/typedpython.md` §4.3 diff --git a/docs/investigations/typedpython-vs-jax.md b/docs/investigations/typedpython-vs-jax.md new file mode 100644 index 000000000..6254d5f2b --- /dev/null +++ b/docs/investigations/typedpython-vs-jax.md @@ -0,0 +1,121 @@ +# TypedPython 과 JAX 비교 (조사일 2026-10-04) + +결론의 반영: `docs/design/typedpython.md` §5.10 (2026-10-04). 메인테이너 결정: §5 의 권고 2 는 "torch 가 JAX 만큼 성능을 내는 부분만 torch 에 맡기고 나머지는 목록으로 따로 판단" 으로 바뀌었고, 권고 4 는 "numpy 의존 없음, 순수 파이썬 참조" 로 정해졌다. + +태그: [V] 가져온 URL 또는 실행한 명령, [R] 읽은 로컬 파일, [I] 추론. 웹 도구가 페이지를 요약해서 돌려주므로 +[V] 항목은 원문 인용이 아니라 요약을 본 것이다. 기기에서 직접 재지 않았다. + +## 1. 비교표 + +| 항목 | JAX (0.11.2, 2026-09-17 [V]) | TypedPython (설계안 [R]) | +|---|---|---| +| 프로그래밍 모델 | `jit` 가 tracer 로 함수를 jaxpr 로 변환. 순수 함수, 불변 배열 [V] | 일반 파이썬 의미를 보존. 컴파일은 IR 에서 증명, 실패하면 인터프리터에 남김 [R §4.3.1] | +| 파이썬 제어 흐름 | trace 시점에 평가. 값 의존 분기는 에러, `static_argnums` 는 재컴파일 [V] | 컴파일 구간에서도 CPython 과 같은 결과가 계약 [R §4.3.4] | +| 부작용 | `print`, list append 는 trace 때만 실행되고 사라짐 [V] | 보존 (같은 결과 원칙) [R] | +| 커널 | Pallas. GPU 는 Mosaic GPU (Hopper 이상), Triton 은 0.11.0 에서 deprecated, TPU 는 Mosaic [V] | `@kernel`, 타일 우선 + SIMT 폴백, Vulkan/Metal/WebGPU/CUDA 출력기 [R §5] | +| 커널의 CPU 실행 | `interpret=True`. 커널을 JAX 함수로 낮춰 grid 위 `scan` 을 `jit` 한 것 [V] | 커널이 수정 없이 CPython 에서 실행 [R §5.2] (설계, 미구현) | +| 모바일 GPU 커널 | Pallas 변경 기록에 Vulkan/Metal 없음 [V] | 1차 목표 (Vulkan, Metal) [R] | +| AOT | `jax.export` 로 StableHLO 직렬화. 내장 플랫폼 TPU, CPU, CUDA, ROCm [V] | 빌드 타임 AOT 가 전제 (iOS) [R §5.1] | +| 실행 런타임 | XLA/PJRT 또는 StableHLO 소비자 (IREE, Google AI Edge) [V] | torchnative 장치 계층 (ash/Vulkan, candle-metal) [R §5.6] | +| 전체 프로그램 융합 | XLA 가 jit 단위로 융합 [I, 일반 지식. 이번에 1차 출처를 확인하지 못함] | 없음. 그래프 캡처로 미룸 (§5.9) [R] | +| autodiff / vmap / sharding | 핵심 기능 [I, 일반 지식. 확인 못 함] | 없음 (설계에 언급 없음 [R]) | +| 정적 타입 | 배열 shape/dtype 은 `jaxtyping` 런타임 검사. 정적 체커는 `Array` 로만 취급 [V] | 빌드 기본 게이트, `Any` 유출 0 (Pyrefly) [R §4.2] | +| 자유 스레드 CPython | 3.14t, 3.15t 지원 표기, mac 포함 wheel [V 검색 요약] | 3.14t 데스크톱 opt-in, 컴파일 모듈은 GIL 재활성 [R INTENT, §4.3.4] | +| iOS/Android 휠 | 변경 기록에 언급 없음 [V]. jaxlib 휠은 Linux, macOS ARM64, Windows [V 검색 요약] | 임베드 대상 [R] | +| HF transformers | v5 에서 Flax/TF 클래스 제거, PyTorch 단일 백엔드 [V] | torch op 로 착지해 transformers 무수정 사용 [R §5.6] | + +## 2. 우리 설계가 이기는 지점 (근거가 버티는 것만) + +1. **모바일 GPU 가 1급 대상이다.** JAX 의 GPU 커널 경로는 Mosaic GPU 가 Hopper 이상 NVIDIA 로 한정되고, Triton + 은 deprecated 다 [V]. Pallas 변경 기록에 Vulkan/Metal 이 없다 [V]. 공식 Apple GPU 플러그인은 "experimental" 이고 + macOS 15.6 에서 `arange` 도 실패하는 이슈가 열려 있다 (jax-metal 0.1.1) [V]. 서드파티 metaljax 가 있으나 + 비공식 beta 이고 mlx-lm/llama.cpp 에 1.1~1.9배 뒤진다고 스스로 적는다 [V]. Android 에 대해서는 이번 조사에서 + 근거를 찾지 못했다 [I: JAX 공식 경로로는 없다고 보이나 증명은 아니다]. +2. **iOS/Android 에서 실행되는 런타임이 있다는 점.** JAX 모델의 모바일 배포는 jax2tf 를 거쳐 TF SavedModel, + LiteRT 로 가는 2단계 경로가 문서화돼 있고 jax2tf 는 experimental 이다 [V]. 임의의 Pallas 커널이 그 경로로 + 가는지는 확인하지 못했다. 그리고 `jax.export` 문서는 안정성이 보장되는 custom call 이 일부뿐이고 나머지는 + 안전 검사를 꺼야 직렬화된다고 한다 [V]. Pallas 커널은 custom call 로 내려가므로 [I] 이식 가능한 아티팩트가 + 되기 어렵다고 추정한다. +3. **정적 타입 게이트는 JAX 에 대응물이 없다.** jaxtyping 은 의도적으로 정적 shape 검사를 하지 않고, 정적 + 체커에는 `Array` 로 보인다. 런타임(trace 시점) 검사다 [V]. 우리의 "사용자 코드에 `Any` 없음" 보장은 다른 + 종류의 보장이다 [I]. 다만 이것이 JAX 대비 우위인 것은 "빌드 실패" 라는 시점뿐이며, shape 오류를 정적으로 + 잡는다는 뜻이 아니다 (우리도 못 한다). +4. **파이썬 의미를 바꾸지 않는다.** JAX 는 `jit` 안에서 값 의존 분기 에러, 부작용 소실, 인덱스 clamp (NumPy 는 + 에러), list 입력 거부, 기본 32비트 정밀도 등 다른 의미로 동작한다 [V]. TypedPython 의 계약은 "어떤 유효한 + CPython 실행과 구별 불가" [R §4.3.4] 이므로 기존 파이썬 코드를 의미 변경 없이 점진 컴파일한다는 점이 다르다. + 다만 이것은 설계 약속이고, 커널 쪽(타일 연산의 CPU 참조 구현)은 아직 구현이 없다 [R]. +5. **CPU 에서 "진짜 파이썬으로" 디버깅.** Pallas `interpret=True` 는 커널을 JAX 함수로 낮춘 `scan` 을 `jit` + 한 것이고, CPU 에서 Pallas 를 돌리는 유일한 방법이다 [V]. 커널 본문은 `Ref` 와 `pl.*` 연산을 쓰므로 JAX 없이는 + 실행되지 않는다 [I]. `jax.debug.print` 같은 JAX 도구가 쓰인다 [V]. 우리 설계는 JAX 도 필요 없이 CPython 만으로 + 돈다는 점에서 한 걸음 더 가깝다. 단, 그 "일반 파이썬 실행" 이 느리다는 비용은 둘 다 같다 [I]. +6. **생태계 방향.** transformers v5 가 Flax/TF 를 제거하고 PyTorch 단일 백엔드가 됐다 [V]. torch op 로 착지하는 + 우리 경로가 HF 생태계와 맞는다. JAX 쪽은 MaxText 등 별도 도구로 호환을 유지한다고 한다 [V]. + +## 3. JAX 가 이기는 지점과 따라잡으려면 + +| JAX 의 강점 | 우리 상태 | 격차를 메우려면 | +|---|---|---| +| XLA 전체 프로그램 융합 | 없음 (§5.9) [R] | torchnative 그래프 캡처 (torchnative #68). 단기에는 수작업 융합 커널 번들 | +| `grad`, `vmap`, `jit` 합성 | 설계 범위 밖 [R] | torch autograd 에 맡긴다고 명시. 커널은 forward/backward 를 사용자가 `torch.library` 로 등록 [I] | +| 샤딩, 다중 기기 (pjit, shard_map) | 없음 | 모바일/온디바이스 목표상 범위 밖으로 선언하는 것이 정직하다 [I] | +| 성숙도, TPU/Hopper 성능 | 코드 없음 (커널 단계 4부터) [R] | 데스크톱 CUDA 비교는 Triton/TileLang 기준 [R §5.8] | +| 타일 커널 DSL 의 검증된 파이프라이닝, warp specialization | 설계만 | Mosaic GPU 문서 (파이프라이닝, warp specialization, Blackwell matmul) [V] 를 참조 사례로 삼는다 | +| `jax.export` 의 호환 창 (후방 6개월, 전방 3주) [V] | 아티팩트 형식 미정 | 커널 번들에 IR 버전과 출력기 버전을 박는다 [I] | +| 자유 스레드 휠 | 3.14t 만 | JAX 는 3.14t, 3.15t 표기 [V]. 우리 MLIR 보조 휠은 3.15t 없음 [R §5.4] | + +## 4. 우위가 불분명하거나 입증되지 않은 것 + +- **"런타임 코드 생성 없음" 은 JAX 대비 우위가 아닐 수 있다.** 이유는 `jax.export` 의 StableHLO 를 IREE 가 AOT + 컴파일해 모바일에 싣는 길이 이론상 있기 때문이다 [V: IREE 와 Google AI Edge 가 StableHLO 소비자]. 실제로 JAX + 모델이 IREE 로 iOS/Android GPU 에서 도는 사례와 성능은 확인하지 못했다. 우리 조사도 IREE 를 "SPIR-V 코드 + 생성만 떼어 쓸 공식 경로 없음" 으로 기각했으나 [R §5.4], 그것은 커널 컴파일러로서의 평가이고 "모델을 통째로 + 배포하는 경로" 로서는 JAX+IREE 가 경쟁자다 [I]. +- **성능.** 둘 다 모바일에서 측정이 없다. llama.cpp/MLX 근접 목표는 안 [R §5.8]. metaljax 가 1.1~1.9배 뒤진다고 + 밝힌 수치가 있어 [V], 우리가 "JAX 보다 빠르다" 고 주장할 근거는 지금 없다. +- **"Pythonic 하다".** JAX 도 NumPy API 이고 학습 곡선이 낮다. 의미 보존의 가치는 기존 코드 이식 시나리오 + 에서만 나타난다 [I]. +- **타입 게이트의 가치.** JAX 사용자가 이 게이트를 원하는지(특히 `jnp` 중심 코드는 `Array` 로 충분히 추론되는지) + 검증하지 않았다 [I]. Pyrefly 가 JAX 를 쓰는 대규모 코드베이스에서 쓰인다는 서술 [R §4.1] 은 JAX 코드가 + 게이트를 통과한다는 뜻이 아니다. +- **Mosaic GPU 와의 직접 대결.** Hopper/Blackwell 한정이라 겹치는 기기가 없다. 우리 데스크톱 CUDA(PTX) 경로가 + JAX 보다 낫다는 주장은 근거가 없다 [I]. +- **Pallas 의 Vulkan/Metal 부재** 는 "변경 기록에 없다" 까지만 확인했다. 실험 브랜치나 제3자 포크는 못 봤다 [V 한계]. + +## 5. 설계 변경 권고 + +1. **포지셔닝 문장을 바꾼다.** "JAX 보다 낫다" 가 아니라 "JAX 가 가지 않는 곳(모바일 Vulkan/Metal, 앱에 임베드된 + CPython, 의미 보존)" 으로 쓴다. 근거가 이 범위에서만 서기 때문이다 (§2.1~2.4). +2. **범위 밖 선언을 문서에 넣는다.** autodiff, vmap, 샤딩은 torch 에 위임하고 만들지 않는다고 §5 에 한 단락 적는다. + 사용자가 JAX 와 같은 것을 기대하지 않게 한다. +3. **`interpret=True` 대응 성질을 시험으로 박는다.** "커널이 JAX/torch 없이 CPython 만으로 도는가" 를 첫 조각 시험에 + 넣는다. 이것이 Pallas 대비 구체적 차별점이므로 구현 전에 반증 가능해야 한다 (AGENTS.md §14). +4. **타일 연산의 CPU 참조 구현 의존성을 정한다.** numpy 를 쓰면 "의존 없는 CPython" 우위가 약해진다. 순수 파이썬 + 참조를 쓸지, 배열 라이브러리를 허용할지 결정이 필요하다 [I]. +5. **IREE 경로를 "모델 단위 대안" 으로 §5.4 에 한 줄 비교로 추가**한다. 기각 사유를 커널 컴파일러 관점과 모델 배포 + 관점으로 나눠 적는다. +6. **JAX 사용자 이주 경로 검토(선택).** 필요하면 StableHLO 입력을 받는 것은 INTENT §1 범위를 넘으므로 제안하지 + 않는다. 메인테이너가 정할 일이다. +7. **융합 격차를 "알려진 약점" 으로 표에 유지**하고 torchnative #68 완료 전에는 모델 전체 속도를 비교 대상으로 + 삼지 않는다 (§5.9 와 같다). + +## 6. 이 방법이 찾지 못하는 것 + +웹 요약 도구가 돌려준 문장에 의존했고(원문 대조 없음), 기기 실측, 비공개 Google 로드맵, 공식 문서에 없는 제3자 +Vulkan/Metal Pallas 포크, 그리고 XLA 융합/autodiff/vmap 의 1차 출처 확인(이번엔 일반 지식으로만 적음)은 찾지 +못한다. + +## 출처 (가져온 것) + +- https://docs.jax.dev/en/latest/changelog.html (0.11.2, Python 3.12 이상, 3.14t/3.15t, 모바일 언급 없음) +- https://docs.jax.dev/en/latest/pallas/CHANGELOG.html (Triton deprecated, Vulkan/Metal 없음) +- https://docs.jax.dev/en/latest/pallas/quickstart.html (Mosaic GPU, Hopper 이상, Triton 비권장, Ref) +- https://docs.jax.dev/en/latest/pallas/design/design.html (scan 기반 emulation) +- https://docs.jax.dev/en/latest/export/export.html , https://docs.jax.dev/en/latest/jax.export.html +- https://docs.jax.dev/en/latest/jit-compilation.html , .../notebooks/Common_Gotchas_in_JAX.html +- https://docs.jax.dev/en/latest/installation.html (Apple GPU experimental) +- https://github.com/jax-ml/jax/issues/34109 (jax-metal 실패) +- https://pypi.org/project/metaljax/ (비공식, 1.1~1.9배) +- https://docs.kidger.site/jaxtyping/faq/ +- https://github.com/openxla/stablehlo/blob/main/README.md (IREE, Google AI Edge) +- https://developers.google.com/edge/litert/conversion/jax/overview (jax2tf 경로, 검색 결과 요약) +- 웹 검색 요약: transformers v5 Flax 제거, jaxlib 3.14t 휠, pallas interpret 파라미터 diff --git a/docs/locale/README_ko.md b/docs/locale/README_ko.md index dff48d279..2627bfaa4 100644 --- a/docs/locale/README_ko.md +++ b/docs/locale/README_ko.md @@ -4,9 +4,9 @@ # 🐍 python-multiplatform -**Kotlin Multiplatform 안의 진짜 CPython — Kotlin 이 Python 을, Python 이 Kotlin 을, 모든 타깃에서 부른다.** +**Kotlin Multiplatform 안의 진짜 CPython, Kotlin 이 Python 을, Python 이 Kotlin 을, 모든 타깃에서 부른다.** -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE) +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../../LICENSE) [![Kotlin Multiplatform](https://img.shields.io/badge/Kotlin-Multiplatform-7F52FF.svg?logo=kotlin&logoColor=white)](https://kotlinlang.org/docs/multiplatform.html) [![CPython](https://img.shields.io/badge/CPython-3.14-3776AB.svg?logo=python&logoColor=white)](https://www.python.org/) [![Platforms](https://img.shields.io/badge/platforms-JVM%20%7C%20Android%20%7C%20iOS%20%7C%20Android%20native%20%7C%20wasm%20(exp.)-lightgrey.svg)](#-플랫폼) @@ -21,31 +21,31 @@ ## 💡 왜 필요한가 -라이브러리는 Python 에 있고, 닿는 범위는 Kotlin Multiplatform 에 있다 — 데스크톱, Android, iOS, 웹. +라이브러리는 Python 에 있고, 닿는 범위는 Kotlin Multiplatform 에 있다, 데스크톱, Android, iOS, 웹. python-multiplatform 은 **진짜 CPython 인터프리터**를 Kotlin Multiplatform 앱 안에 넣고, 그 위에 양방향 다리를 놓는다. - Kotlin 은 Python 위에 **타입이 있는, Kotlin 다운 객체 모델**을 얻는다. `PyList` 는 곧 `MutableList` 이고, `PyDict` 는 곧 `MutableMap` 이며, Python 오류는 곧 `Throwable` 이다. - Python 은 **Kotlin 클래스를 평범한 Python 으로** 쓴다. `Greeter('Kotlin').greet(2)`, - `suspend fun` 에 대한 `await`, 메서드로 보이는 Kotlin 확장 함수 — 모두 빌드 타임에 생성한 테이블을 + `suspend fun` 에 대한 `await`, 메서드로 보이는 Kotlin 확장 함수, 모두 빌드 타임에 생성한 테이블을 거치므로, 리플렉션이 없는 곳(Kotlin/Native, GraalVM 네이티브 이미지)에서도 동작한다. 재구현이 아니라 CPython 그 자체이므로 C 확장도 그대로 동작한다. ## ✨ 기능 -- 🔌 **다운콜** — CPython Stable ABI(약 330개 함수)를 `expect` 하나로 선언하고 플랫폼마다 `actual` 을 +- 🔌 **다운콜**: CPython Stable ABI(약 330개 함수)를 `expect` 하나로 선언하고 플랫폼마다 `actual` 을 둔다. 데스크톱은 Panama `invokeExact`, Android 는 `RegisterNatives` JNI, Native 는 cinterop. -- 🧩 **객체 모델** — `PyObject`, `PyType`, `PyException`, 기본 타입, Kotlin 컬렉션 인터페이스를 구현하는 +- 🧩 **객체 모델**: `PyObject`, `PyType`, `PyException`, 기본 타입, Kotlin 컬렉션 인터페이스를 구현하는 `PyList` / `PyDict` / `PySet` / `PyTuple`, 모듈, 호출 가능 객체, 변환 계층. -- 🚀 **업콜** — KSP 프로세서가 함수 테이블을 생성한다. Python 이 Kotlin 클래스를 생성하고, 메서드를 +- 🚀 **업콜**: KSP 프로세서가 함수 테이블을 생성한다. Python 이 Kotlin 클래스를 생성하고, 메서드를 부르고, 프로퍼티를 읽고 쓰고, `suspend` 함수를 취소까지 포함해 `await` 한다. -- 📦 **빌드된 라이브러리 바인딩** — Gradle 플러그인이 jar(Compose 포함)를 훑어 바인딩과 `.pyi` 스텁을 +- 📦 **빌드된 라이브러리 바인딩**: Gradle 플러그인이 jar(Compose 포함)를 훑어 바인딩과 `.pyi` 스텁을 생성한다. 기본 인자 생략과 값 클래스를 지원한다. -- ♻️ **수동 메모리 관리 없음** — 래퍼는 수거될 때 Python 참조를 놓는다. 경계를 가로지르는 참조 순환은 +- ♻️ **수동 메모리 관리 없음**: 래퍼는 수거될 때 Python 참조를 놓는다. 경계를 가로지르는 참조 순환은 Python 의 순환 GC 가 수거한다. -- 🧪 **측정됨** — 모든 경계 메커니즘에 오버헤드 테스트가 붙어 있고, 비용 표는 실제 실행에서 렌더링된다 +- 🧪 **측정됨**: 모든 경계 메커니즘에 오버헤드 테스트가 붙어 있고, 비용 표는 실제 실행에서 렌더링된다 ([비용 표](../investigations/cost-table.md)). ## 🚀 빠른 시작 @@ -117,7 +117,7 @@ async def main(): ### 플랫폼별 준비
    -데스크톱 — PYTHONHOME 에 stdlib 가 있도록 Gradle 플러그인을 적용한다 +데스크톱, PYTHONHOME 에 stdlib 가 있도록 Gradle 플러그인을 적용한다 ```kotlin plugins { @@ -136,7 +136,7 @@ plugins {
    -Android — PythonBootstrap.initialize 를 호출한다 +Android, PythonBootstrap.initialize 를 호출한다 ```kotlin import python.multiplatform.env.PythonBootstrap @@ -170,7 +170,7 @@ flowchart LR T --> K ``` -플랫폼마다 진입점은 하나이고, 그 뒤에서는 **모든 플랫폼이 같은 생성 테이블을 쓴다** — 플랫폼별 +플랫폼마다 진입점은 하나이고, 그 뒤에서는 **모든 플랫폼이 같은 생성 테이블을 쓴다**, 플랫폼별 Python → Kotlin 바인딩 코드가 없다. 자세한 내용: [설계 문서](../design/). ## 🌍 플랫폼 @@ -186,7 +186,7 @@ Python → Kotlin 바인딩 코드가 없다. 자세한 내용: [설계 문서]( ## 📊 상태 -정직한 요약이다 — 남은 작업은 [로드맵](../roadmap/ROADMAP.md) 에 있다. +정직한 요약이다, 남은 작업은 [로드맵](../roadmap/ROADMAP.md) 에 있다. | 영역 | 상태 | |---|---| @@ -211,10 +211,10 @@ Python → Kotlin 바인딩 코드가 없다. 자세한 내용: [설계 문서]( ## 📖 문서 -- 🌐 **[가이드](https://thisisthepy.github.io/python-multiplatform/)** (English / 한국어) — 소스는 [`docs/guide/`](../guide/) -- 🏗 [설계 문서](../design/) — 구조, 다운콜, 업콜, 수명, 스레딩, 노출 정책 -- 📱 [플랫폼 문서](../platforms/) — Android FFM, wasm, CPython 취득, GraalVM 검증 -- 🔬 [조사 기록](../investigations/) — 측정과 감사 +- 🌐 **[가이드](https://thisisthepy.github.io/python-multiplatform/)** (English / 한국어), 소스는 [`docs/guide/`](../guide/) +- 🏗 [설계 문서](../design/), 구조, 다운콜, 업콜, 수명, 스레딩, 노출 정책 +- 📱 [플랫폼 문서](../platforms/), Android FFM, wasm, CPython 취득, GraalVM 검증 +- 🔬 [조사 기록](../investigations/), 측정과 감사 - 🗺 [로드맵](../roadmap/ROADMAP.md) - 🇺🇸 [영어 README](../../README.md) @@ -227,4 +227,4 @@ iOS 타깃 빌드에는 Xcode 가 있는 macOS 가 필요하다. ## 📄 라이선스 -[MIT](../../LICENSE) © 2024 thisisthepy +[Apache-2.0](../../LICENSE) © 2024 thisisthepy diff --git a/docs/platforms/desktop-packaged-app.md b/docs/platforms/desktop-packaged-app.md index ca24c6889..7f803b020 100644 --- a/docs/platforms/desktop-packaged-app.md +++ b/docs/platforms/desktop-packaged-app.md @@ -11,8 +11,8 @@ app now gets its prefix, why it was done this way, and how to check it. ## What happens **Build time** (`python-multiplatform-gradle-plugin`, `PythonBindingsPlugin.configurePackagedPythonHome`). -Every Compose Desktop `prepare*AppResources` task — the `Sync` that `createDistributable`, -`packageDmg`/`packageMsi`/`packageDeb` and their `Release` variants take their resources from — +Every Compose Desktop `prepare*AppResources` task, the `Sync` that `createDistributable`, +`packageDmg`/`packageMsi`/`packageDeb` and their `Release` variants take their resources from, depends on `stagePythonHome` and copies part of the staged prefix into `python-multiplatform-home/`: | Host | Copied (relative to the prefix) | @@ -30,7 +30,7 @@ In the app image the directory is `$APPDIR/resources/python-multiplatform-home`; **Run time** (`python-multiplatform`). `PackagedPythonHome.resolve()` (`jvmMain`) takes the first of: -1. `PYTHONHOME` in the environment — then nothing else happens; CPython reads it itself. +1. `PYTHONHOME` in the environment, then nothing else happens; CPython reads it itself. 2. The system property `python.multiplatform.home`, used even if the path does not exist, so that a wrong value is reported by name. 3. `/python-multiplatform-home`, only if that directory exists. @@ -58,7 +58,7 @@ mechanism for the stdlib was considered and not taken: abort at `Py_Initialize` rather than a re-extraction. (Not observed here; a reason not to take the risk for the one tree whose absence is fatal.) - `libpython` has to be a file for `System.load` anyway. Loading it from the classpath copy means - extracting 19.4 MB into the working directory and finding it through `java.library.path` — this + extracting 19.4 MB into the working directory and finding it through `java.library.path`, this repo's tests pass `-Djava.library.path=.`, a launched app has no such flag and may have `/` as its working directory. Loading it from the packaged prefix also pairs it with its own stdlib. - jpackage builds an image for the host it runs on, so only one platform's prefix is ever packaged; diff --git a/docs/platforms/free-threaded-mobile.md b/docs/platforms/free-threaded-mobile.md new file mode 100644 index 000000000..8146231ae --- /dev/null +++ b/docs/platforms/free-threaded-mobile.md @@ -0,0 +1,120 @@ +# Free-threaded CPython for Android and iOS + +Status: build recipe written from the CPython 3.15.0 source; **not yet run in CI, +not yet run on a device or emulator.** Tracking issue: #163. + +python-multiplatform gets its parallelism from free-threaded CPython (decision 8 in +`AGENTS.md`). Desktop uses the python.org 3.14t builds. No prebuilt 3.15t exists for +Android or iOS, so the workflow `.github/workflows/build-ft-mobile.yml` builds it with +CPython's own mobile tooling. + +## What is built + +| Platform | Host | Tooling | Output | +|---|---|---|---| +| Android | `aarch64-linux-android` | `Platforms/Android` | `python-3.15.0-aarch64-linux-android.tar.gz` | +| iOS | `arm64-apple-ios` (device), `arm64-apple-ios-simulator`, `x86_64-apple-ios-simulator` | `Platforms/Apple` | `python-3.15.0-iOS-XCframework.tar.gz` | + +The iOS tooling only builds an XCframework from all of its slices, so the simulator +hosts are built as well as the device. There is no cheaper arm64-only path in the +script. + +## How `--disable-gil` is passed + +Both scripts accept extra `configure` arguments after `--` +(`Platforms/Android/__main__.py`: `"args", nargs="*", help="Extra arguments to pass to +`configure`"`; `Platforms/Apple/__main__.py`: `args", nargs="*", help="Extra arguments +to pass to `configure`"`). The workflow uses the discrete steps so that the flag reaches +both the build Python and every host: + +```sh +python3 Platforms/Android configure-build -- --disable-gil +python3 Platforms/Android make-build +python3 Platforms/Android configure-host aarch64-linux-android -- --disable-gil +python3 Platforms/Android make-host aarch64-linux-android +python3 Platforms/Android package aarch64-linux-android + +python3 Platforms/Apple configure-build -- --disable-gil +python3 Platforms/Apple make-build +python3 Platforms/Apple configure-host iOS -- --disable-gil # per host +python3 Platforms/Apple make-host iOS +python3 Platforms/Apple package iOS +``` + +`configure.ac` turns the flag into `#define Py_GIL_DISABLED 1` and appends `t` to +`ABIFLAGS`, so the library, include directory and standard library directory become +`libpython3.15t`, `include/python3.15t` and `lib/python3.15t`. `--disable-gil` needs +mimalloc, which is on by default. + +The build Python is also built with `--disable-gil`. The cross-build only requires the +same major.minor version (`--with-build-python`), so a GIL build Python would probably +work too, but matching them removes a variable. + +## Scripts + +`.github/scripts/ft-mobile/`: + +- `common.sh`: source download, `Py_GIL_DISABLED` check, sha256 recording. +- `build-android.sh [VERSION]`: unpacks `Python-.tgz`, installs the NDK the + tooling pins (`ndk_version=` in `Platforms/Android/android-env.sh`, 27.3.13750724 in + 3.15.0), runs the steps above, then verifies the package. +- `build-ios.sh [VERSION]`: same for iOS, with the two edits below. + +Local use is possible on a Linux or macOS machine with the prerequisites, but the heavy +build is meant for CI (`AGENTS.md` rule 10). Both scripts honour `FT_WORK` and `FT_OUT` +(default under `.tmp/`). + +### Android notes + +- `Platforms/Android package` lists files with `git ls-files`, which does not work in an + unpacked tarball. `build-android.sh` runs `git init` and `git add Platforms/Android` + in the source tree first. +- The package contains `prefix/include/python3.15t/` and `prefix/lib/libpython3.15t.so` + plus `lib/python3.15t/`, OpenSSL and SQLite libraries, and the `android.py` helper. +- Dependencies (OpenSSL, SQLite, libffi, xz, bzip2, zstd) come from the + `beeware/cpython-android-source-deps` releases, as in upstream. + +### iOS notes + +- Upstream `Platforms/Apple/__main__.py` computes `version_tag` as `3.15`, but a + free-threaded framework installs `lib/python3.15t`. Unpatched, `package` fails when it + copies the `.xcprivacy` files into `lib-/python3.15/lib-dynload`. `build-ios.sh` + appends `t` to `version_tag`; this also names the symlink `lib/libpython3.15t.dylib`. +- `testbed/Python.xcframework/build/utils.sh` finds the standard library with + `grep -E "^python3\.\d+$"`, which does not match `python3.15t`. `build-ios.sh` changes + it to `^python3\.[0-9]+t?$`. Each edit is asserted, so an upstream change fails the + build instead of skipping the patch. +- The framework and its binary are still named `Python` / `Python.framework/Python`; + only the `lib` side carries the `t`. The `Py_GIL_DISABLED` define is in + `Headers/pyconfig-.h`; `Headers/pyconfig.h` is a wrapper that includes it. +- Dependencies come from `beeware/cpython-apple-source-deps` releases. The runner needs + a full Xcode (`macos-15`); the Xcode version is printed in the log. + +## Verification built into the workflow + +Each script fails the job unless: + +- the build Python reports `Py_GIL_DISABLED == 1`; +- Android: `include/python3.15t/pyconfig.h` has `#define Py_GIL_DISABLED 1`, + `lib/libpython3.15t*.so` exists and is an aarch64 ELF, `lib/python3.15t` exists; +- iOS: every slice has `Headers/pyconfig-.h` with `#define Py_GIL_DISABLED 1`, + `lib/libpython3.15t.dylib`, and a `Python.framework/Python` binary (`lipo -info` + is logged); the XCframework has `lib/python3.15t` and exactly two slices. + +The sha256 of each archive is written to the job summary and the archives are uploaded +as workflow artifacts (`python-t-android-aarch64`, +`python-t-ios-xcframework`). + +## Not verified + +- The workflow has not run yet. Whether 3.15.0 configures and compiles with + `--disable-gil` for these targets is unknown; free-threaded support on Android and iOS + is not a documented CPython target. +- Nothing has been loaded on a device, emulator or simulator. No import of `_ssl`, + `_ctypes` or extension modules, no GIL-off test run, no thread-safety check. +- Whether the prebuilt dependency tarballs and the Xcode on `macos-15` suit 3.15.0 is + unconfirmed. +- Integration with the Gradle plugin's `PYTHONHOME` staging, with the Android + `libpython` loading code, and with the iOS framework embedding (which expects + `Python.framework` and a non-`t` library name in places) is not done. +- The x86_64 simulator slice is built only because the packaging step requires it. diff --git a/docs/platforms/graal-native-image-verification.md b/docs/platforms/graal-native-image-verification.md index 12ccbb28c..5a6d22f9d 100644 --- a/docs/platforms/graal-native-image-verification.md +++ b/docs/platforms/graal-native-image-verification.md @@ -71,18 +71,18 @@ the plugin's KMP support, not a native-image limitation. - **Date:** 2026-08-17 - **Branch / commit:** `work/graal` off `develop` @ `4687070a` (*Merge branch 'work/syspath' into develop*) - **Worktree:** `/Volumes/macMini/worktrees/graal` -- **Machine note:** other agents were building concurrently — load average was **10.39** and physical +- **Machine note:** other agents were building concurrently, load average was **10.39** and physical memory was nearly exhausted (129 MB unused of 16 GB) when `nativeCompile` started. It still succeeded, and the pass/fail results below are unaffected by contention. **The timings, however, are not clean benchmarks and should not be quoted as such.** -### Step 1 — JVM control +### Step 1, JVM control ``` :sample:runNativeImageUpcallDemo EXIT=0 BUILD SUCCESSFUL in 42s ``` -### Step 2 — Native image build +### Step 2, Native image build ``` :sample:nativeCompile EXIT=0 BUILD SUCCESSFUL in 46s @@ -96,18 +96,22 @@ the plugin's KMP support, not a native-image limitation. Finished generating 'upcall-native-demo' in 43.5s. ``` -Artifact: `sample/build/native/nativeCompile/upcall-native-demo` — **26,317,504 bytes (25.1 MB)**, +The "registered for reflection" line counts everything in the image, the JDK's, Kotlin's and +every library's shipped metadata included, not what this repository registers. Of it, this +repository contributes one class, `UpcallTarget` (see the result table; corrected in #244). + +Artifact: `sample/build/native/nativeCompile/upcall-native-demo`, **26,317,504 bytes (25.1 MB)**, `Mach-O 64-bit executable arm64`. The `28 downcalls and 5 upcalls` the image registered match **exactly** the counts in the *generated* `reachability-metadata.json` (`python-multiplatform/build/generated/native-image-metadata/desktop/...`, `foreign.downcalls` = 28, `foreign.upcalls` = 5). The image consumed the generated metadata rather than a checked-in file that -could have rotted out of sync — which is the failure mode `generateDesktopReachabilityMetadata` +could have rotted out of sync, which is the failure mode `generateDesktopReachabilityMetadata` exists to prevent, since an undeclared descriptor builds clean and dies at the first call with `MissingForeignRegistrationError`. -### Step 3 — Native image execution +### Step 3, Native image execution ``` :sample:runNativeUpcallDemo EXIT=0 BUILD SUCCESSFUL in 3s @@ -151,7 +155,7 @@ completer thread -> clean PYTHON: PROXY_OK ``` -### Step 4 — JVM vs native diff +### Step 4, JVM vs native diff The two runs are the **same commit and the same entry point**, so they are directly comparable. This is the control that matters; comparing against a ROADMAP snapshot from an older commit (as an @@ -171,15 +175,15 @@ Diffing the two 32-line output blocks: ``` **29 of 32 lines are byte-identical.** The only three that differ are raw runtime stub addresses, -which carry no semantics. Every line that asserts behaviour — the table size, the resolved handles, +which carry no semantics. Every line that asserts behaviour, the table size, the resolved handles, `invoke result = 7`, `invoke_args result = 'presses x3 = 21'`, the `-1` for `@PythonInternal`, all -of the proxy assertions, and both markers — is identical. +of the proxy assertions, and both markers, is identical. Incidental observation: under native-image the three Panama upcall stubs land at **contiguous** addresses 0x40 apart, whereas on the JVM they are scattered. Consistent with stubs being laid out into one image region at build time rather than allocated on demand. -### Step 5 — Closed-world proof +### Step 5, Closed-world proof Running through Gradle leaves open the objection that a JVM was somewhere in the picture. It was not. The binary was executed **directly, with `JAVA_HOME` removed from the environment**: @@ -217,7 +221,7 @@ rather than been produced silently. | Generated Python proxy layer works | **YES** (`PROXY_OK`, including `await` over a `suspend fun` settled from a Kotlin thread) | | `@PythonInternal` opt-out still enforced | **YES** (resolves to `-1`) | | Behaviour differs from JVM | **NO** (29/32 lines identical; the 3 differing lines are runtime addresses) | -| Reflection registration needed for the upcall path | **NO** — the path uses none, which is the §7 design premise holding | +| Reflection registration needed for the upcall path | **One class, by hand** (corrected in #244): the generated function table, which is every Kotlin declaration Python calls, needs none, which is the §7 premise holding; the upcall *bootstrap* finds its five FFM entry points by name on one fixed class of this repository (`UpcallStub.kt`, `UpcallTarget::class.java.methods`), so `python.native.ffi.UpcallTarget` is registered with `allPublicMethods` in `python-multiplatform/native-image/reachability-metadata.base.json` | Nothing is outstanding on the desktop target. The untested surface is other targets: this verifies **desktop only**, as `sample/build.gradle.kts` wires `nativeCompile` from the `desktop` compilation. diff --git a/docs/platforms/ios-app-bundle.md b/docs/platforms/ios-app-bundle.md index daf78fd55..1ddcaf218 100644 --- a/docs/platforms/ios-app-bundle.md +++ b/docs/platforms/ios-app-bundle.md @@ -33,14 +33,14 @@ PYTHON_DYLIB_INFO_TEMPLATE=/python-multiplatform/build/python-ios-home/ipho A universal `ARCHS` (`arm64 x86_64`) is refused with the fix (`ONLY_ACTIVE_ARCH=YES` or `ARCHS=arm64`): one prefix holds one `lib-dynload`. Upstream's `utils.sh` has the same limit without saying so. -**Xcode** (`tools/xcode/install-python.sh`, a Run Script phase after Copy Bundle Resources and before +**Xcode** (`python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`, a Run Script phase after Copy Bundle Resources and before Embed Frameworks; `ENABLE_USER_SCRIPT_SANDBOXING = NO`). -1. Runs Gradle as above. The output is captured, then parsed, then checked to be a directory — a +1. Runs Gradle as above. The output is captured, then parsed, then checked to be a directory, a failing Gradle stops the phase and never leaves `rsync` an empty source (issue #59's comment). 2. `rsync -a --delete` the prefix to `/python-multiplatform-home/`. 3. The payload, if configured, to `/python/` (`--delete`, without `__pycache__`): - `PYTHON_PAYLOAD_DIR=` (a directory whose contents are the payload root — the app's own Python + `PYTHON_PAYLOAD_DIR=` (a directory whose contents are the payload root, the app's own Python sources, or toolchain's `build/pythonStaging/ios/python`), or `PYTHON_PAYLOAD_TASK=` that prints `PYTHON_PAYLOAD_DIR=` under `-q` (toolchain's `:app:stagePythonBundleIosForXcode`). Neither set: no payload, and an old `/python/` is removed. @@ -53,7 +53,7 @@ Embed Frameworks; `ENABLE_USER_SCRIPT_SANDBOXING = NO`). `Python3.initialize()` calls `applyPackagedPythonHome()` after the `PYTHONHOME` pre-flight check (L-3) and before `Py_Initialize()`. `IosPythonHome.resolve()` takes the first of: -1. `PYTHONHOME` in the environment — nothing else happens. The simulator test task and an Xcode +1. `PYTHONHOME` in the environment, nothing else happens. The simulator test task and an Xcode scheme variable keep working as before. 2. `/python-multiplatform-home`, only if that directory exists. @@ -90,23 +90,28 @@ before. `PyConfig.home` is the non-deprecated route, but the library carries no ## Wiring an app -The sample's project is `iosApp/iosApp.xcodeproj` (product `PythonDemo.app`, bundle id +The sample's project is `sample/src/iosMain/app.xcodeproj` (target `app`, product `PythonDemo.app`, bundle id `org.thisisthepy.python.multiplatform.demo`). Its phase: ```sh set -e -cd "$SRCROOT/.." -export PYTHON_PAYLOAD_DIR="$SRCROOT/../sample/python/src/main" -/bin/bash tools/xcode/install-python.sh +cd "$SRCROOT/../../.." +export PYTHON_PAYLOAD_DIR="$SRCROOT/../commonMain/python" +/bin/bash python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh ``` It replaced two phases: "Install Target Specific Python Standard Library", which rsynced the slice's -`lib/` — only `libpython3.14.dylib` — into `/lib/`, and "Prepare Python Binary Modules", which -did the framework wrapping from a template copied in as a resource. `iosApp/iosApp/dylib-Info-template.plist` -is no longer copied into the app; the file itself is still in the project. - -`sample/src/iosMain/app.xcodeproj` is not wired: it still references `../../dist/toolchain/...` and -a `ComposeApp` framework that this build does not produce, and has never been built here. +`lib/`, only `libpython3.14.dylib`, into `/lib/`, and "Prepare Python Binary Modules", which +did the framework wrapping from a template copied in as a resource. The template is no longer copied +into the app: `install-python.sh` takes the one the Gradle plugin stages (`PYTHON_DYLIB_INFO_TEMPLATE`). + +The project used to live in a root `iosApp/` folder. It moved to `sample/src/iosMain/` (the +app-template layout, #107); the old `python` folder reference to `build/pythonStaging/ios/python` +and the `../../dist/toolchain/...` references were removed, because `install-python.sh` already +copies the payload to `/python/` and a second copy would conflict. The target runs two Gradle +tasks first (`:python-multiplatform:embedAndSignAppleFrameworkForXcode`, +`:sample:embedAndSignAppleFrameworkForXcode`), then compiles, then installs Python, then embeds +`Python.xcframework`. A consumer outside this repository cannot call `:python-multiplatform:stageIosPythonHomeForXcode`: the iOS archive download lives in this repository's build script, not in the published plugin. That @@ -138,14 +143,14 @@ EFFECTIVE_PLATFORM_NAME=-iphonesimulator ARCHS="arm64 x86_64" \ # 2. Build the app (flags from the last verified run, ROADMAP history 2026-08-13: actool cannot # compile the app icon against this machine's simulator runtimes, and -target avoids # -destination's eligibility check) -(cd iosApp && xcodebuild -project iosApp.xcodeproj -target iosApp -configuration Debug \ - -sdk iphonesimulator ARCHS=arm64 ONLY_ACTIVE_ARCH=YES SYMROOT="$PWD/build" \ +xcodebuild -project sample/src/iosMain/app.xcodeproj -target app -configuration Debug \ + -sdk iphonesimulator ARCHS=arm64 ONLY_ACTIVE_ARCH=YES SYMROOT="$PWD/sample/build/xcode-app" \ ASSETCATALOG_COMPILER_APPICON_NAME="" \ - CODE_SIGN_IDENTITY="-" CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM="" build) \ + CODE_SIGN_IDENTITY="-" CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM="" build \ > .tmp/xcodebuild.log 2>&1; echo "EXIT=$?" grep -E "Installing the Python|Wrapped|error:" .tmp/xcodebuild.log -APP=iosApp/build/Debug-iphonesimulator/PythonDemo.app +APP=sample/build/xcode-app/Debug-iphonesimulator/PythonDemo.app ls "$APP/python-multiplatform-home/lib/python3.14/os.py" "$APP/python/example_py/__init__.py" ls "$APP/python-multiplatform-home/lib/python3.14/lib-dynload" | grep -c '\.fwork$' # 67 for 3.14 find "$APP/python-multiplatform-home" "$APP/python" -name '*.so' | wc -l # 0 @@ -175,7 +180,7 @@ Fail shapes and what they mean: | Seen | Meaning | |---|---| -| No `DEMO` lines, 0% CPU (`sample ` parked in `open$NOCANCEL`) | the app read a path outside its container — check `env` for a stray `PYTHONHOME` | +| No `DEMO` lines, 0% CPU (`sample ` parked in `open$NOCANCEL`) | the app read a path outside its container, check `env` for a stray `PYTHONHOME` | | `Fatal Python error: Failed to import encodings module` | no `python-multiplatform-home/` in the installed app; the phase did not run | | `DEMO 8 bundle | ModuleNotFoundError: No module named '_json'` | the `.fwork` did not resolve; compare `sys.executable`'s directory with the app directory | | `DEMO 8 bundle | ModuleNotFoundError: No module named 'example_py'` | `/python/` missing: `PYTHON_PAYLOAD_DIR` not set in the phase | diff --git a/docs/platforms/python-version-acquisition.md b/docs/platforms/python-version-acquisition.md index daa234cc2..544c13d3f 100644 --- a/docs/platforms/python-version-acquisition.md +++ b/docs/platforms/python-version-acquisition.md @@ -51,18 +51,18 @@ The project currently uses `python-build-standalone` (maintained by Astral). For ## 5. Security and Integrity Verification * **Desktop:** `python-build-standalone` provides a `SHA256SUMS` file with every release. The build script verifies the downloaded archive against this manifest. -* **Android (python.org) and iOS (python.org, 3.15+) — Sigstore verified.** This bullet used to say that "full Sigstore verification directly inside Gradle is unreasonable without shelling out to external tools (like `sigstore-python` or the `cosign` CLI)". **That was wrong**, and it is now implemented. `dev.sigstore:sigstore-java` does the whole verification in-process — certificate chain, Rekor inclusion and OIDC identity — with no external binary. python.org publishes a sibling `.sigstore` bundle for both Android tarballs and, from 3.15, the iOS XCframework. (The extension is `.sigstore`; `.sigstore.json` returns 404.) There is still no plain checksum manifest, so the lockfile continues to pin the digest as well. +* **Android (python.org) and iOS (python.org, 3.15+), Sigstore verified.** This bullet used to say that "full Sigstore verification directly inside Gradle is unreasonable without shelling out to external tools (like `sigstore-python` or the `cosign` CLI)". **That was wrong**, and it is now implemented. `dev.sigstore:sigstore-java` does the whole verification in-process, certificate chain, Rekor inclusion and OIDC identity, with no external binary. python.org publishes a sibling `.sigstore` bundle for both Android tarballs and, from 3.15, the iOS XCframework. (The extension is `.sigstore`; `.sigstore.json` returns 404.) There is still no plain checksum manifest, so the lockfile continues to pin the digest as well. * **The signer identity is pinned, and has to be version-keyed.** Verifying a bundle *without* pinning an identity proves only that somebody holding a Sigstore certificate signed the bytes, which anyone can arrange. The build pins the Fulcio SAN and OIDC issuer of the actual CPython release manager, per . That must be a map rather than a constant, because it changes per release series: 3.14/3.15 are `hugo@python.org` via `https://github.com/login/oauth`, whereas 3.12/3.13 are `thomas@python.org` via `https://accounts.google.com`. An unrecorded series is a hard build failure, never a silent skip. -* **Opt-in — `-PverifyPythonSignatures=true`.** Off by default because `sigstore-java` pulls in grpc-netty-shaded, protobuf, bouncycastle and guava, and because fetching the TUF trust root needs network access, which would turn an offline build from working into failing. Gradle resolves the configuration lazily, so a default build downloads none of it (measured: a default `downloadPython_android_*` run mentions Sigstore zero times). -* **Desktop (`python-build-standalone`) — Sigstore verified via GitHub Attestations.** The release carries 853 assets and the only non-archive among them is `SHA256SUMS`: there is no `.sigstore` file. Provenance is published through GitHub's *attestations* API instead, keyed by artifact **digest** rather than filename, and that endpoint is rate-limited to 60 requests/hour unauthenticated. Because of this rate limit and the bundle size (~212KB each), the build does not check these bundles into the repository. Instead, when `-PverifyPythonSignatures=true` is set, it fetches the DSSE bundle from the API, caches it locally as `.sigstore`, and uses `sigstore-java` to verify it against the pinned `astral-sh/python-build-standalone` workflow identity. Desktop is therefore protected by the lockfile, the release's own `SHA256SUMS`, and Sigstore. -* **iOS (BeeWare, ≤ 3.14; `3.14-b11` pin `b591f330...ab7b`, taken from GitHub's asset digest and recomputed locally) — nothing exists to verify against.** The `Python-Apple-support` releases publish five `tar.gz` assets and nothing else: no checksums, no signatures, and no GitHub attestations. The lockfile pin is the only honest instrument available here, and no amount of build wiring changes that. -* **The lockfile is not superseded by any of this.** The two gates prove different things — the lockfile says "these are the exact bytes this repository reviewed and pinned", Sigstore says "these are the bytes the release manager actually signed". The lockfile is also the only check that works offline and the only one covering every source, so it stays unconditional and Sigstore is layered on top of it. +* **Opt-in, `-PverifyPythonSignatures=true`.** Off by default because `sigstore-java` pulls in grpc-netty-shaded, protobuf, bouncycastle and guava, and because fetching the TUF trust root needs network access, which would turn an offline build from working into failing. Gradle resolves the configuration lazily, so a default build downloads none of it (measured: a default `downloadPython_android_*` run mentions Sigstore zero times). +* **Desktop (`python-build-standalone`), Sigstore verified via GitHub Attestations.** The release carries 853 assets and the only non-archive among them is `SHA256SUMS`: there is no `.sigstore` file. Provenance is published through GitHub's *attestations* API instead, keyed by artifact **digest** rather than filename, and that endpoint is rate-limited to 60 requests/hour unauthenticated. Because of this rate limit and the bundle size (~212KB each), the build does not check these bundles into the repository. Instead, when `-PverifyPythonSignatures=true` is set, it fetches the DSSE bundle from the API, caches it locally as `.sigstore`, and uses `sigstore-java` to verify it against the pinned `astral-sh/python-build-standalone` workflow identity. Desktop is therefore protected by the lockfile, the release's own `SHA256SUMS`, and Sigstore. +* **iOS (BeeWare, ≤ 3.14; `3.14-b11` pin `b591f330...ab7b`, taken from GitHub's asset digest and recomputed locally), nothing exists to verify against.** The `Python-Apple-support` releases publish five `tar.gz` assets and nothing else: no checksums, no signatures, and no GitHub attestations. The lockfile pin is the only honest instrument available here, and no amount of build wiring changes that. +* **The lockfile is not superseded by any of this.** The two gates prove different things, the lockfile says "these are the exact bytes this repository reviewed and pinned", Sigstore says "these are the bytes the release manager actually signed". The lockfile is also the only check that works offline and the only one covering every source, so it stays unconditional and Sigstore is layered on top of it. * **The verification is known to fail when it should**, which is the only thing that makes it worth having. Two negative controls were run against the real 3.14.7 Android bundle: flipping one base64 character in `messageSignature.signature` gave `KeylessVerificationException: Artifact signature was not valid`, and pointing the identity map at the 3.13 release manager gave `No provided certificate identities matched values in certificate`. Both failed the build with a non-zero exit. ## 6. WebAssembly No distributor ships a `python.wasm` that exports `wasmExports` and `wasmMemory` to JavaScript, which the -`wasmJs` target needs. CPython is therefore **built**, not downloaded: `tools/wasm/build-cpython.sh` builds +`wasmJs` target needs. CPython is therefore **built**, not downloaded: `python-multiplatform/scripts/wasm/build-cpython.sh` builds CPython 3.14.2 for `wasm32-emscripten` (Emscripten 5.0.3, matched to the `pyemscripten_2026_0` platform of PEP 783) under the git-ignored `.caches/`, and `python-multiplatform-wasm-runtime` is the published zip of the result. This is deliberately a different patch release from the native `pythonVersion`; see diff --git a/docs/platforms/wasm-design.md b/docs/platforms/wasm-design.md index 414eb1b67..d0d385609 100644 --- a/docs/platforms/wasm-design.md +++ b/docs/platforms/wasm-design.md @@ -4,8 +4,8 @@ How CPython runs under Kotlin/Wasm in this library, why it has this shape, and w get there. The rules a contributor must follow are in [`python-multiplatform/src/wasmJsMain/README.md`](../../python-multiplatform/src/wasmJsMain/README.md); the behavioural contract is `docs/SPEC.md` (§0, C-6, M-3, U-5). This document is the design record. -The chronological experiment log it was distilled from — including the stretch where wasm was -"parked" — is [`../archive/wasm-design-experiment-log.md`](../archive/wasm-design-experiment-log.md). +The chronological experiment log it was distilled from, including the stretch where wasm was +"parked", is [`../archive/wasm-design-experiment-log.md`](../archive/wasm-design-experiment-log.md). **Status: integrated and experimental.** `wasmJs` is a Kotlin target (`python-multiplatform/build.gradle.kts`), `src/wasmJsMain` implements the `EmbedAPI` `actual`s, and `src/wasmJsTest` runs the suite on Node @@ -16,9 +16,9 @@ has a browser wasm target. `wasmWasi` is not a target (when last assessed it was | Decision | Choice | Why | |---|---|---| -| How Kotlin reaches CPython | Raw Stable ABI symbols as `@WasmImport`s against the Emscripten module's own wasm exports — **no JS in the call path** | Preserves the exact object model (`NativePointer`, `PyObject`) of every other target; a high-level Pyodide-style JS API (`pyodide.runPython`, `PyProxy`) would replace the whole object model with `JsAny` wrappers and covers almost none of the ~330 C functions | -| Memory | **One shared linear memory.** Kotlin imports Emscripten's `Module.wasmMemory` as its `intrinsics.memory` | Needs Kotlin ≥ 2.4.20-Beta2, whose `wasmJs` modules **import** `intrinsics.memory` (`min=0, max=none`). Releases up to 2.4.10 exported an unbounded memory, which Emscripten cannot import — the earlier plan to patch the `.wasm` memory section (and a YouTrack issue) is obsolete ([`../archive/wasm-youtrack-issue.md`](../archive/wasm-youtrack-issue.md)) | -| Where CPython comes from | **Our own Emscripten build**, `tools/wasm/build-cpython.sh`: CPython **3.14.2**, Emscripten **5.0.3**, matched to the `pyemscripten_2026_0` platform of PEP 783 | No distributor ships a `python.wasm` exporting `wasmExports,wasmMemory`. The version is deliberately not the 3.14.x pinned for native targets (`pythonVersion` in `gradle.properties`); see `docs/design/ecosystem.md` | +| How Kotlin reaches CPython | Raw Stable ABI symbols as `@WasmImport`s against the Emscripten module's own wasm exports, **no JS in the call path** | Preserves the exact object model (`NativePointer`, `PyObject`) of every other target; a high-level Pyodide-style JS API (`pyodide.runPython`, `PyProxy`) would replace the whole object model with `JsAny` wrappers and covers almost none of the ~330 C functions | +| Memory | **One shared linear memory.** Kotlin imports Emscripten's `Module.wasmMemory` as its `intrinsics.memory` | Needs Kotlin ≥ 2.4.20-Beta2, whose `wasmJs` modules **import** `intrinsics.memory` (`min=0, max=none`). Releases up to 2.4.10 exported an unbounded memory, which Emscripten cannot import, the earlier plan to patch the `.wasm` memory section (and a YouTrack issue) is obsolete ([`../archive/wasm-youtrack-issue.md`](../archive/wasm-youtrack-issue.md)) | +| Where CPython comes from | **Our own Emscripten build**, `python-multiplatform/scripts/wasm/build-cpython.sh`: CPython **3.14.2**, Emscripten **5.0.3**, matched to the `pyemscripten_2026_0` platform of PEP 783 | No distributor ships a `python.wasm` exporting `wasmExports,wasmMemory`. The version is deliberately not the 3.14.x pinned for native targets (`pythonVersion` in `gradle.properties`); see `docs/design/ecosystem.md` | | C shim / composition | **None** | The hops that composition amortised are gone; measured gain ≤ 2.9 ns per operation (below). It also removes a second build pipeline | | `EXPORTED_FUNCTIONS` list | **Not needed** | `-sMAIN_MODULE` is `LINKABLE`, so the whole Stable ABI is exported (all 310 C symbols `EmbedAPI.kt` declared at the time were found in the build's 8287 exports). The build adds `wasmExports,wasmMemory` to `-sEXPORTED_RUNTIME_METHODS`, which is not ABI-sensitive | | Upcalls (Python → Kotlin) | `@WasmExport` trampolines placed in CPython's `__indirect_function_table` with `WebAssembly.Table.set`; the table index is the C function pointer. Routing is by data (a handle in `self`/`closure`), through the generated table, as on every platform | A funcref is a funcref: `call_indirect` reaches the Kotlin export directly. Measured 3.1 ns vs 10.9 ns for the `addFunction` + JS-closure route | @@ -44,7 +44,7 @@ has a browser wasm target. `wasmWasi` is not a target (when last assessed it was cold paths. - The generated entry module is patched at build time (`patchKotlinWasmOutputForCPython`): one substitution for the memory, one for the upcall handoff (`pmpSetKotlinExports(exports)`, before - `exports._start()`). An application must also declare the three `@WasmExport` trampolines itself — + `exports._start()`). An application must also declare the three `@WasmExport` trampolines itself, `@WasmExport` is honoured only in the compilation that produces the `.wasm` (`ProxyTypeExportNames`; generating that file from the Gradle plugin is not done). - **JSPI must not reach the embedding.** `cpython.mjs` deletes `WebAssembly.promising` / @@ -65,9 +65,9 @@ current figures: run `WasmCrossingOverheadTest`, `WasmUpcallRouteOverheadTest`, | Bulk read, 1000 `i32`: shared memory `Pointer.loadInt()` vs `Module.HEAP32` | 0.7 vs 7.1 ns per element | | Composition against the interpreter, reading one global (200 000×): naive / + interned strings / + hoisted module+dict | 259.5 / 185.4 / **65.2** ns; a composed call would save 2.9 ns of that | | Upcall, 10 M indirect calls issued from C: `table.set` vs `addFunction` + JS closure vs C→C control | **3.1** vs 10.9 vs 0.7 ns | -| Kotlin `METH_O` function vs C builtin `abs` through `PyMethodDef` | 87 vs 73 ns (14 ns is the JS frame CPython's own trampoline imposes — see below) | +| Kotlin `METH_O` function vs C builtin `abs` through `PyMethodDef` | 87 vs 73 ns (14 ns is the JS frame CPython's own trampoline imposes, see below) | | String argument, `"version"` (7 chars) / 54 chars: scratch vs interned | 30.6 / 157.0 ns vs 22.2 / 26.4 ns (`WasmMarshallingOverheadTest`, 200 000 iterations) | -| `char*` → `String`, 27 / 4000 bytes: ASCII `CharArray`+`concatToString()` vs `ByteArray`+`decodeToString()` | 103 / 5 875 ns vs 405 / **58 271** ns — never `decodeToString()` a large buffer | +| `char*` → `String`, 27 / 4000 bytes: ASCII `CharArray`+`concatToString()` vs `ByteArray`+`decodeToString()` | 103 / 5 875 ns vs 405 / **58 271** ns, never `decodeToString()` a large buffer | Corrections that came out of measuring (they changed the design): @@ -86,22 +86,22 @@ Corrections that came out of measuring (they changed the design): ## The interpreter build and the platform tag -`tools/wasm/build-cpython.sh` builds CPython 3.14.2 with CPython's own `Tools/wasm/emscripten` driver +`python-multiplatform/scripts/wasm/build-cpython.sh` builds CPython 3.14.2 with CPython's own `Tools/wasm/emscripten` driver plus the changes that make it claim `pyemscripten_2026_0` (PEP 783): | | stock Tier 3 build | this build | |---|---|---| -| Unwinding ABI `-fwasm-exceptions -sSUPPORT_LONGJMP=wasm` at compile **and** link | absent | **present — this is the one that gates loading a compiled wheel** (the side module imports the `__cpp_exception` tag) | +| Unwinding ABI `-fwasm-exceptions -sSUPPORT_LONGJMP=wasm` at compile **and** link | absent | **present, this is the one that gates loading a compiled wheel** (the side module imports the `__cpp_exception` tag) | | `PYEMSCRIPTEN_PLATFORM_VERSION` | not defined anywhere in CPython 3.14.2 | defined via a patch to `sysconfig` (`_ALWAYS_STR`; otherwise `int('2026_0') == 20260` silently yields the tag `pyemscripten_20260_wasm32`) | | `wasmExports,wasmMemory,wasmTable` in `-sEXPORTED_RUNTIME_METHODS` | no | yes (not ABI-sensitive) | -| `lzma`, `zstd`, OpenSSL | missing | **still missing** — ABI-sensitive per Pyodide's flag list but they did not gate the wheel tested; costs `_lzma`, `_zstd`, `_hashlib`, `_ssl` | +| `lzma`, `zstd`, OpenSSL | missing | **still missing**, ABI-sensitive per Pyodide's flag list but they did not gate the wheel tested; costs `_lzma`, `_zstd`, `_hashlib`, `_ssl` | Evidence: a real PyPI wheel, `pydantic_core-2.48.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl`, imports -and runs on this build (`validate_python(42) -> 42`, a `ValidationError` raised and caught — the +and runs on this build (`validate_python(42) -> 42`, a `ValidationError` raised and caught, the exception exercises the unwinding ABI), and fails with `tag import requires a WebAssembly.Tag` on the stock build. This is one wheel and one code path; it does not show that every compiled extension -loads. `WasmCompiledWheelTest` runs it (`tools/wasm/build-cpython.sh wheels` fetches the pinned wheels); -`WasmInterpreterAbiTest` checks the claims, with `tools/wasm/build-cpython.sh stock` as the negative +loads. `WasmCompiledWheelTest` runs it (`python-multiplatform/scripts/wasm/build-cpython.sh wheels` fetches the pinned wheels); +`WasmInterpreterAbiTest` checks the claims, with `python-multiplatform/scripts/wasm/build-cpython.sh stock` as the negative control. Emscripten is pinned to 5.0.3 because that is what the platform specifies, and installing it replaces `~/emsdk/upstream` in place (a global setting). diff --git a/docs/roadmap/ROADMAP.md b/docs/roadmap/ROADMAP.md index 1e4a2a30b..0cc69cb33 100644 --- a/docs/roadmap/ROADMAP.md +++ b/docs/roadmap/ROADMAP.md @@ -2,17 +2,17 @@ **What is left to do in this repository, and nothing else.** -- **Behaviour status** — what is implemented, partial or planned, with the tests that assert it — +- **Behaviour status**: what is implemented, partial or planned, with the tests that assert it, is in [`../SPEC.md`](../SPEC.md). This file does not repeat it; it lists the work that remains and points at the test or document that proves the part already done. -- **History** — how each item was found, measured and closed — stays in git. The last full version +- **History**: how each item was found, measured and closed, stays in git. The last full version of the old progress log is `4ed98143:docs/roadmap/ROADMAP.md`; the whole trail is `git log -p -- ROADMAP.md docs/roadmap/ROADMAP.md`. -- **Section numbers 1–16 are the old ones**, kept because other files cite them ("ROADMAP §9", +- **Section numbers 1–16 are the old ones**: kept because other files cite them ("ROADMAP §9", "§15d", …). A closed section keeps its heading and one line saying where its evidence is. The old sub-sections (§2a…§16f) are mapped at the head of each section. -- Each remaining item says: what is left, its status (**open** — not started or not working; - **partial** — part done, the rest named; **unverified** — could not be decided from code and tests), +- Each remaining item says: what is left, its status (**open**, not started or not working; + **partial**, part done, the rest named; **unverified**, could not be decided from code and tests), and the evidence path. Paths are relative to the repository root; `PM` = `python-multiplatform/src`, `GP` = `python-multiplatform-gradle-plugin/src`. - Established against code and tests on 2026-10-03 (`develop` @ `5db4168c`) **without running @@ -27,7 +27,7 @@ (`PM/commonMain/kotlin/python/multiplatform/ffi/Python3.kt`, `initialize`); background cleaners release through `withGIL` (`PM/commonTest/.../ref/GCLeakTest.kt`). -- **1.1 A test that fails when a C API call runs without the GIL — partial.** Every C API call is +- **1.1 A test that fails when a C API call runs without the GIL, partial.** Every C API call is supposed to hold the GIL (AGENTS.md §16), and three past regressions were exactly an unguarded call that only crashed once the GIL was really released. The only direct test, `PM/commonTest/.../ffi/GilParkingTest.kt`, asserts "does not crash". What is left is a guard that @@ -49,7 +49,7 @@ are deliberate (`ffiAllocUtf8`/`ffiFreeUtf8`/`ffiReadUtf8`, `echoCriticalNamed`) `docs/investigations/jni-call-convention-audit.md`. Measured reason no function was promoted to `@CriticalNative`: promoting costs +16.54 ns on API 34 and +25.73 ns on API 36. -- **3.1 `JniWiringTest.bothListGetItemConventionsAgree` on a device — unverified.** The +- **3.1 `JniWiringTest.bothListGetItemConventionsAgree` on a device, unverified.** The `PyList_GetItem` `@FastNative` twin was compile-verified when added; whether this assertion has passed on `pmp_api26` and `pmp_api36` is not recorded per test. Undecided registrations (65 at the last count) stay on ordinary JNI by default, which is the safe direction; no action is needed for @@ -62,34 +62,34 @@ Closed for the ordinary case: wrappers release on GC on every target `PM/androidInstrumentedTest/.../ref/AndroidCleanerPathTest.kt`), and cross-boundary cycles with a Python-side edge are collected (§7, SPEC M-3). -- **4.1 Cycles that close entirely on the Kotlin side — open.** `HandleTable` holds every entry +- **4.1 Cycles that close entirely on the Kotlin side, open.** `HandleTable` holds every entry strongly by construction (`PM/commonMain/kotlin/python/multiplatform/reflection/HandleTable.kt`, KDoc "This table leaks by construction"), so a cycle with no Python-side edge for `tp_traverse` to report never collects. The mechanism (weak table entries, the proxy supplying the strength) is named but not worked through in `docs/design/object-lifetime.md`. -- **4.2 Wrappers that outlive `Py_Finalize()` — open (known limitation).** Their cleaners see +- **4.2 Wrappers that outlive `Py_Finalize()`, open (known limitation).** Their cleaners see `!Python3.isInitialized` and skip the release (`Python3.finalize`), which is safe at shutdown but leaves stale pointers if the interpreter is ever initialised again. No re-initialisation test exists; `finalize` is `internal` and documented as "will not be able to re-initialize". ## 5. Composition (조립) -String-carrying operations: **closed** — interning beats composing on every API level (`getAttr` +String-carrying operations: **closed**, interning beats composing on every API level (`getAttr` 160 ns on API 36, 167 ns on API 26 interned; `docs/design/marshalling-design.md`). -- **5.1 Bulk iteration through a composed call — open (optional optimisation).** Composition pays +- **5.1 Bulk iteration through a composed call, open (optional optimisation).** Composition pays only where crossings scale with N: `list → LongArray` of 1000 elements measured 11.0x faster composed on API 36 hardware (50 065.89 → 4 532.55 ns, `PM/androidInstrumentedTest/kotlin/python/native/ffi/CompositionBenchmark.kt`). The probe (`asmListToArray`, `PM/androidMain/.../bindings.kt`) exists; no production path uses it. Also left: `PM/artMain/kotlin/python/multiplatform/ffi/JniExport.kt` still exports composed `Python3` - functions that nothing on the JVM side calls — decide whether to wire or delete them. + functions that nothing on the JVM side calls, decide whether to wire or delete them. -## 6. Composition on desktop — measured, and the answer is no +## 6. Composition on desktop, measured, and the answer is no **Closed** (composing would buy ~10 % of an `exec`; not worth new native targets). -- **6.1 `DesktopOverheadBenchmark.stringMarshallingShareOfARealisticCall` is `@Ignore`d — open.** +- **6.1 `DesktopOverheadBenchmark.stringMarshallingShareOfARealisticCall` is `@Ignore`d, open.** Running `exec` 200 000 times aborts the JVM inside `PyDict_New`; clearing the error indicator on failure paths recovered most of it but the abort's cause is not pinned (`PM/desktopTest/kotlin/python/native/ffi/DesktopOverheadBenchmark.kt`). Re-enable only once @@ -99,42 +99,42 @@ String-carrying operations: **closed** — interning beats composing on every AP Closed in its core: build-time table, KSP generator, one entry point per platform, generated proxies, `tp_traverse` handle slot on all five targets, GraalVM native image (SPEC U-1…U-6, M-3, M-4; -`ksp-fixtures/app/src/desktopTest/`, `PM/commonTest/.../reflection/UpcallTableTest.kt`, +`python-multiplatform-ksp/fixtures/app/src/desktopTest/`, `PM/commonTest/.../reflection/UpcallTableTest.kt`, `PM/*Test/.../ref/CycleCollectionTest.kt`). Upcall cost is measured (`PM/commonTest/.../reflection/UpcallOverheadTest.kt`, `PM/commonTest/.../ffi/upcall/GeneratedProxyCostTest.kt`). -- **7.1 Generated proxies exercised outside desktop — partial.** SPEC U-3: the full proxy behaviour +- **7.1 Generated proxies exercised outside desktop, partial.** SPEC U-3: the full proxy behaviour (construct, methods, properties, companions) is asserted on desktop only; outside it there are - `ksp-fixtures/app/src/androidNativeArm64Test/.../NativeSmokeTest.kt` and - `ksp-fixtures/android/.../GeneratedAndroidTableTest.kt`. The KSP-generated-proxy cycle proof - (`ksp-fixtures/app/src/desktopTest/.../RefHolderCycleCollectionTest.kt`) likewise exists on desktop - only, because `ksp-fixtures/app` has no iOS or device leaf; other targets prove the slot with a + `python-multiplatform-ksp/fixtures/app/src/androidNativeArm64Test/.../NativeSmokeTest.kt` and + `python-multiplatform-ksp/fixtures/android/.../GeneratedAndroidTableTest.kt`. The KSP-generated-proxy cycle proof + (`python-multiplatform-ksp/fixtures/app/src/desktopTest/.../RefHolderCycleCollectionTest.kt`) likewise exists on desktop + only, because `python-multiplatform-ksp/fixtures/app` has no iOS or device leaf; other targets prove the slot with a hand-built subclass (`PM/nativeTest/.../CycleCollectionTest.kt`). -- **7.2 Extension functions as methods on KSP-generated proxies — partial.** SPEC U-7: asserted for a +- **7.2 Extension functions as methods on KSP-generated proxies, partial.** SPEC U-7: asserted for a hand-written table (`PM/commonTest/.../pythonx/PythonxAdapterTest.kt`) and for walked jars - (`ksp-fixtures/artifact/.../WalkedArtifactComposeModifierTest.kt`); not for a table KSP generated. -- **7.3 Incremental KSP — open (deferred design change).** A one-file change regenerates the + (`python-multiplatform-gradle-plugin/fixtures/artifact/.../WalkedArtifactComposeModifierTest.kt`); not for a table KSP generated. +- **7.3 Incremental KSP, open (deferred design change).** A one-file change regenerates the module's whole `Fragment_` (100 % dirty, measured with `ksp.incremental.log`). The only route is per-file fragments, which changes fragment naming and `UpcallTable`'s duplicate detection; `docs/design/upcall.md` §2.7. -- **7.4 Binary-size cost of blacklist exposure — open (product decision).** A table referencing every +- **7.4 Binary-size cost of blacklist exposure, open (product decision).** A table referencing every `public` declaration defeats dead-code elimination: ≈1.84 KB per entry in a stripped `androidNativeArm64` binary (200 synthetic entries, 368 640 bytes; `docs/design/upcall.md` §2.6). The measurement informs, but does not settle, whether that is acceptable. -- **7.5 Silent exclusions without a KSP warning — open.** A `suspend` function *type* in a signature +- **7.5 Silent exclusions without a KSP warning, open.** A `suspend` function *type* in a signature now warns (`FragmentScanner.warnUnexposableType`); a declaration dropped by `BindingPolicy.isExposedFunctionShape` (an extension receiver, type parameters) is still dropped with no warning. `docs/design/upcall.md` §8 item 4. -- **7.6 Kotlin calling a Python override — open.** Python can subclass a proxy, but a Kotlin caller of a +- **7.6 Kotlin calling a Python override, open.** Python can subclass a proxy, but a Kotlin caller of a virtual method never reaches the Python override; nothing implements or asserts it (`docs/design/upcall.md` §4.4). -- **7.7 `BYTES` marshalling is per item — open.** `PyBytes_AsStringAndSize` is not declared in +- **7.7 `BYTES` marshalling is per item, open.** `PyBytes_AsStringAndSize` is not declared in `EmbedAPI.kt`, so a `ByteArray` crosses one byte at a time; correct (NUL bytes survive, SPEC U-2) but slow (`docs/design/upcall.md` §3.3). -- **7.8 Fragment discovery from transitive and published artefacts — unverified.** Discovery is proven +- **7.8 Fragment discovery from transitive and published artefacts, unverified.** Discovery is proven for project modules and a `.klib` (`NativeSmokeTest`), not for a fragment arriving through a transitive Maven dependency (`docs/design/upcall.md` §2.9). -- **7.9 Proxy-cost tables predate the 100 000-iteration warmup — open (re-measure).** +- **7.9 Proxy-cost tables predate the 100 000-iteration warmup, open (re-measure).** `docs/design/upcall.md` §7.5/§7.6 absolutes were taken before the warmup fix; re-cut with `GeneratedProxyCostTest` through `./benchmarks/cost-table.sh`. Android/androidNative proxy-cost rows are missing. @@ -145,9 +145,9 @@ Closed in its core: build-time table, KSP generator, one entry point per platfor `PyValueLazyConversionTest.kt`). `complex`, builtin subclasses, `memoryview` and user-defined classes are refused by design (SPEC O-7). -- **7b.1 NATIVE conversion of a user-defined object — open (design question).** `pyObjectToNative`'s - fallback returns `str(obj)`, which is lossy and one-way. Three candidates — `__dict__` projection, - an upcall-table handle, or documenting NATIVE as builtins-only — are written at the branch +- **7b.1 NATIVE conversion of a user-defined object, open (design question).** `pyObjectToNative`'s + fallback returns `str(obj)`, which is lossy and one-way. Three candidates, `__dict__` projection, + an upcall-table handle, or documenting NATIVE as builtins-only, are written at the branch (`PM/commonMain/kotlin/python/multiplatform/ffi/types/collections/CollectionSupport.kt`, `TODO(open design question)`), with what to measure first. @@ -169,14 +169,14 @@ The 3.15 migration (`PyWeakref_GetRef`, the two removed functions) is done (`PM/commonTest/kotlin/python/native/ffi/EmbedApiLowLevelTest.kt`), and 3.15.0rc1 checksums, including free-threaded ones, are pinned in `python-checksums.properties`. -- **9.1 `autoDrainInterval`'s default on the GIL build — open (decision).** Today +- **9.1 `autoDrainInterval`'s default on the GIL build, open (decision).** Today `if (BuildConfig.pythonFreeThreaded) 32 else 0` (`Python3.kt`). Off means a pure C-API embedder never runs the cyclic collector: 20 000+ cyclic objects left with the automatic path off against ~1 970 with it on (`docs/investigations/gc-scheduling-investigation.md` §6–§7). On means a `__del__` or weakref callback can run at a `withGIL` exit the caller did not ask to yield from. The checkpoint costs ~128 ns over a 154.6 ns `withGIL` floor, ~4 ns amortised (`EvalCheckpointTest.testCheckpointCostAgainstTheAlternatives`). A one-line change once decided. -- **9.2 Free-threading beyond desktop — open (blocked upstream).** No free-threaded CPython prebuilt +- **9.2 Free-threading beyond desktop, open (blocked upstream).** No free-threaded CPython prebuilt exists for Android (python.org ships `libpython3.14.so`/`3.15.so` only) or iOS (neither BeeWare nor python.org defines `Py_GIL_DISABLED`), checked 2026-08-12. SPEC N-1. @@ -187,34 +187,34 @@ cycles, interning and `Py_ssize_t` are closed (`PM/wasmJsTest/`: `WasmFinalizati `WasmCycleCollectionTest`, `WasmPySsizeTBoundaryTest`, `WasmBrowserRuntimeTest`, `WasmSelectorsImportTest`); the plugin stages the runtime for consumers (`GP/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt`, -`GenerateWasmProxyExportsTask.kt`). The interpreter is built by `tools/wasm/build-cpython.sh` +`GenerateWasmProxyExportsTask.kt`). The interpreter is built by `python-multiplatform/scripts/wasm/build-cpython.sh` (`docs/platforms/wasm-design.md`). Old §10c/§10d (browser test, CI split) are closed; their open remainder is 10.1. -- **10.1 The wasm test suite does not run in CI — open.** `.github/workflows/wasm.yml` runs the +- **10.1 The wasm test suite does not run in CI, open.** `.github/workflows/wasm.yml` runs the `test` job only when the repository variable `WASM_RUNTIME_URL` is set. It is not set (observed 2026-10-03: `gh variable list` empty; the latest `develop` run shows "WasmJS Node + browser tests: skipped", compile job green). What closes it is uploading the `python-multiplatform-wasm-runtime` zip (7.0 MB) somewhere a runner can fetch and setting the variable; see also 15.1. -- **10.2 `suspend` upcalls and `asyncio` on wasm — open.** SPEC U-5 is `planned` on wasm (no threads), +- **10.2 `suspend` upcalls and `asyncio` on wasm, open.** SPEC U-5 is `planned` on wasm (no threads), and `import asyncio` traps the instance (`asyncio.run` wants `socketpair`); the sample skips section 7 there (`sample/src/wasmJsMain/.../bindings/ProxyDemo.wasmJs.kt`). -- **10.3 A wasm consumer must hand-write the `pmp_invoke` export — open.** `@WasmExport` is honoured +- **10.3 A wasm consumer must hand-write the `pmp_invoke` export, open.** `@WasmExport` is honoured only in the compilation that produces the final `.wasm`, so the library cannot export the general upcall dispatcher for a consumer; `GenerateWasmProxyExportsTask` generates only the three proxy-type slots. Without the three-line export (`PM/wasmJsTest/.../UpcallExports.kt` shows it), `UpcallBootstrap.publishToGlobals()` throws. Generating it from the plugin, as was done for the slots, is the obvious step. -- **10.4 A consumer's bundle in a real browser / production bundler — unverified.** The only browser +- **10.4 A consumer's bundle in a real browser / production bundler, unverified.** The only browser execution evidence is the in-repo `:sample` and `wasmJsBrowserTest`. An external consumer (`consumer-plugin-android`) was verified to *build* `wasmJsBrowserDistribution` with the runtime staged by the plugin; it was never run in a browser, and no production bundler other than the Kotlin Gradle webpack setup was tried (also `docs/design/ecosystem.md` §5 "Unverified"). -- **10.5 The wasm CPython build lacks lzma, zstd and OpenSSL — open.** Listed as "NOT DONE" in - `tools/wasm/build-cpython.sh`'s header; ABI-sensitive per the Pyodide flag list, though they did not +- **10.5 The wasm CPython build lacks lzma, zstd and OpenSSL, open.** Listed as "NOT DONE" in + `python-multiplatform/scripts/wasm/build-cpython.sh`'s header; ABI-sensitive per the Pyodide flag list, though they did not gate the one compiled wheel tested (`WasmCompiledWheelTest`). -- **10.6 The sample's wasm upcall line (`presses x3 = 0`) was never cross-checked against desktop — +- **10.6 The sample's wasm upcall line (`presses x3 = 0`) was never cross-checked against desktop, unverified, minor.** ## 11. Build wiring @@ -231,25 +231,25 @@ pre-release URL, `PyList.subList` (a live `PySubList`), the `TODO` triage, `runM (`PythonBootstrapTest.initializeLeavesTempfileUsable`), the `EmbedAPI.kt` section order (sections 27–29 now sit in docs order), stale test headers, the sample revisit. -- **12.1 `Python3.finalize` should call `Py_FinalizeEx` — open.** It still calls `Py_Finalize()` +- **12.1 `Python3.finalize` should call `Py_FinalizeEx`, open.** It still calls `Py_Finalize()` (`Python3.kt`, `finalize`); `Py_FinalizeEx`'s `expect` and every `actual` already exist. Blocked only by finalisation being untestable in a suite that shares one interpreter (its only caller is `PM/artMain/.../ffi/JniExport.kt`). -- **12.2 `TMPDIR` for androidNative inside an app, and on an iOS device — open / unverified.** The +- **12.2 `TMPDIR` for androidNative inside an app, and on an iOS device, open / unverified.** The androidNative suite passes only because `adb shell` sets `TMPDIR=/data/local/tmp`; a binary shipped in an app would have neither `/tmp` nor that variable. iOS passes on the simulator because `/tmp` is the host's; a device needs `NSTemporaryDirectory()`, never run. Probe: the `scriptPath…` cases of `RunAppTest`. -- **12.3 Linux and Windows desktop have never run the suite — open.** `.github/workflows/desktop.yml` +- **12.3 Linux and Windows desktop have never run the suite, open.** `.github/workflows/desktop.yml` has `os: [macos-latest]` only, and no log anywhere shows a Linux or Windows run (SPEC P-2, N-4). Adding the two runners to the matrix is the step. -- **12.4 CI on `develop` is not green — open (observed 2026-10-03).** All four platform workflows now +- **12.4 CI on `develop` is not green, open (observed 2026-10-03).** All four platform workflows now run on GitHub Actions (the old "CI has never run" is closed). The latest `develop` Desktop CI run failed one test, `PyValueBytesConversionTest.bytesConversionCostIsPaidOncePerValueAndIsReportedAgainstStr` (a timing assertion: 1000 cached reads must cost less than one conversion), while the same commit's - PR run passed — a timing assertion that is not stable on a shared runner. Decide whether such + PR run passed, a timing assertion that is not stable on a shared runner. Decide whether such assertions belong in CI. -- **12.5 BeeWare's iOS archives (≤ 3.14) carry no signature or checksum — not fixable here.** The +- **12.5 BeeWare's iOS archives (≤ 3.14) carry no signature or checksum, not fixable here.** The SHA-256 lockfile pin is the only integrity check for that source; python.org's signed XCframework replaces it from 3.15. python-build-standalone likewise publishes no per-file signature (`SHA256SUMS` only). `docs/platforms/python-version-acquisition.md` §5. @@ -257,27 +257,27 @@ pre-release URL, `PyList.subList` (a live `PySubList`), the `TODO` triage, `runM ## 13. The sample, and the AGP version that shapes it The sample runs on desktop, Android (both emulators), the iOS simulator and in a browser; the bindings -plugin applies to Android modules (AGP 8.10.1); `ksp-fixtures/android` exists; the generated +plugin applies to Android modules (AGP 8.10.1); `python-multiplatform-ksp/fixtures/android` exists; the generated `installGeneratedUpcallTable` `actual` reaches intermediate source sets; the per-platform upcall trampolines exist. GraalVM native image upcalls are verified by hand (`docs/platforms/graal-native-image-verification.md`, `PM/desktopTest/.../ReachabilityMetadataTest.kt`). -- **13.1 The iOS app bundle carries no Python standard library — implemented, simulator proof +- **13.1 The iOS app bundle carries no Python standard library, implemented, simulator proof open.** SPEC L-11, issue #59, `docs/platforms/ios-app-bundle.md`: per-slice staging - (`stageIosPythonHome_*`, `stageIosPythonHomeForXcode`), `tools/xcode/install-python.sh` (stdlib to + (`stageIosPythonHome_*`, `stageIosPythonHomeForXcode`), `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh` (stdlib to `/python-multiplatform-home/`, payload to `/python/`, `.so` wrapped as frameworks with - `.fwork` placeholders) replacing `iosApp/`'s two broken phases, and `IosPythonHome` + + `.fwork` placeholders) replacing the two broken phases of the old root `iosApp/` (now `sample/src/iosMain/app.xcodeproj`, #107), and `IosPythonHome` + `Py_SetPythonHome` at run time. Left: the installed-app run on the simulator with no `SIMCTL_CHILD_*` (the page's "Checking it"); a device run; and a way for consumers outside this repository to stage the stdlib (the iOS archive download is in this build script, not the plugin). -- **13.2 Native-image upcall verification as an automated test — open.** SPEC U-6/N-5: the procedure +- **13.2 Native-image upcall verification as an automated test, open.** SPEC U-6/N-5: the procedure is manual (`:sample:nativeCompile`, Liberica NIK); only the reachability metadata is guarded automatically. -- **13.3 iOS sample section 7, slow path — open (sample only).** "not wired on iOS": resuming a parked +- **13.3 iOS sample section 7, slow path, open (sample only).** "not wired on iOS": resuming a parked continuation needs a Kotlin/Native worker the sample does not start (`sample/src/iosMain/.../bindings/ProxyDemo.ios.kt`). The library itself does it (`PM/nativeTest/.../AsyncUpcallNative*Test.kt`). -- **13.4 Targets compiled but never tested — open.** `iosArm64`, `iosX64`, `androidNativeX64` (SPEC +- **13.4 Targets compiled but never tested, open.** `iosArm64`, `iosX64`, `androidNativeX64` (SPEC P-1). For `iosX64`, `extractIosSimulatorStdlib` hardcodes `lib-arm64` (`python-multiplatform/build.gradle.kts`, the `…/ios-arm64_x86_64-simulator/lib-arm64/python$libVersion` copy); an x86_64 simulator needs `lib-x86_64`. Stale text nearby: that task's KDoc still says @@ -300,22 +300,22 @@ stripped), the root metadata jar is 382 KB, `PythonBootstrap` stages the Android desktop stdlib (`GP/test/kotlin/python/multiplatform/gradle/PythonHomeStagingTest.kt`, SPEC L-4). Old §15a, §15b, §15d, §15f, §15g, §15h are closed; §15e item 4 is closed by §15h. -- **15.1 Nothing is published to a remote repository — open.** No publishing repository or signing is +- **15.1 Nothing is published to a remote repository, open.** No publishing repository or signing is configured (`signing { }` is commented out in `python-multiplatform/build.gradle.kts`), and `repo1.maven.org/maven2/io/github/thisisthepy/python-multiplatform/` returns 404 (2026-10-03). Every consumer verification so far used `mavenLocal()`. 10.1 depends on this or on a release asset. -- **15.2 The KSP processor and plugin versions are hand-copied literals — open.** Root +- **15.2 The KSP processor and plugin versions are hand-copied literals, open.** Root `build.gradle.kts` `allprojects { version = "3.13.0" }` (inherited by `python-multiplatform-ksp`) and `python-multiplatform-gradle-plugin/build.gradle.kts` `version = "3.13.0"` agree only by hand, while the library publishes `3.14.7-alpha01` from `gradle.properties`. The plugin bakes `DEFAULT_PROCESSOR_COORDINATES` from its own version, so the two literals drifting would reproduce the "processor coordinate that exists nowhere" defect with no configuration-time error. A check, or one source for both, is what is left. -- **15.3 `stagePythonHome` has run only on `macos-aarch64` — partial.** Linux-x86_64, macOS-x86_64 and +- **15.3 `stagePythonHome` has run only on `macos-aarch64`, partial.** Linux-x86_64, macOS-x86_64 and Windows-x86_64 are pinned as string assertions in `PythonHomeStagingTest`; Windows' `Lib/`+`DLLs/` layout and `linux-aarch64` (no `libpython` in `desktopJar`, relies on `manager.loadFromSidecar`) have never executed. -- **15.4 A packaged desktop application carries no CPython prefix — implemented, end-to-end check +- **15.4 A packaged desktop application carries no CPython prefix, implemented, end-to-end check pending (issue #60, SPEC L-9).** The plugin copies the staged prefix's stdlib and `libpython` into Compose Desktop's `prepare*AppResources` (so `createDistributable` and `package*`) as `python-multiplatform-home/`; at run time the library resolves it through @@ -325,44 +325,44 @@ Old §15a, §15b, §15d, §15f, §15g, §15h are closed; §15e item 4 is closed `docs/platforms/desktop-packaged-app.md`. Left: Conveyor and non-Compose jpackage need the property set by hand; no `.pyc` is shipped; only the build host's platform is packaged (jpackage builds for its host anyway). -- **15.5 The Android AAR duplicates the pure-Python stdlib per ABI — open (measured option, not +- **15.5 The Android AAR duplicates the pure-Python stdlib per ABI, open (measured option, not taken).** 790 files / 11.91 MB are byte-identical between `arm64-v8a` and `x86_64`; de-duplicating saves 3.16 MB compressed but changes the published asset layout `/lib/python` that `PythonBootstrap` and consumers read. ## 16. Bindings from resolved artefacts (the artefact walker) -The walker binds jar declarations — statics, top-level functions, extensions, constructors, value -classes, defaults, callbacks, `@Composable`s — by generating Kotlin source (SPEC B-1, B-3, B-4, B-6; -`GP/test/.../artifact/ArtifactScannerTest.kt`, `ksp-fixtures/artifact/`, `ksp-fixtures/compose/`), +The walker binds jar declarations, statics, top-level functions, extensions, constructors, value +classes, defaults, callbacks, `@Composable`s, by generating Kotlin source (SPEC B-1, B-3, B-4, B-6; +`GP/test/.../artifact/ArtifactScannerTest.kt`, `python-multiplatform-gradle-plugin/fixtures/artifact/`, `python-multiplatform-compose/fixtures/compose/`), reads Kotlin metadata (`GP/main/.../artifact/KotlinMetadata.kt`), walks klibs through an isolated worker (`KlibScanner.kt`, `KlibScanWorkAction.kt`), and takes several targets (`artifactTargets`, `GP/test/.../ArtifactTargetsWiringTest.kt`). Consumers bootstrap `_pm_resolve`/`_pm_invoke` through `UpcallBootstrap` (`PM/commonTest/.../ffi/upcall/UpcallBootstrapTest.kt`). Old §16a–§16d are description, §16e/§16f's open remainder is below. -- **16.1 klib declarations bound at run time — partial.** SPEC B-2/N-3. `KlibScannerTest` reads real - klibs and declines with reasons; `ksp-fixtures/klib-artifact` walks +- **16.1 klib declarations bound at run time, partial.** SPEC B-2/N-3. `KlibScannerTest` reads real + klibs and declines with reasons; `python-multiplatform-gradle-plugin/fixtures/klib-artifact` walks `androidNativeArm64CompileKlibraries` through the plugin, but the namespace it walks (`kotlinx.coroutines`) yields **zero** bindings by design, so a successful read and a silently failed one still produce the same empty `ArtifactTable` - (`ksp-fixtures/klib-artifact/src/androidNativeArm64Test/.../WalkedKlibArtifactTableTest.kt`). And no - Gradle task runs that fixture's tests — they are compiled and linked, and run only by pushing + (`python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/androidNativeArm64Test/.../WalkedKlibArtifactTableTest.kt`). And no + Gradle task runs that fixture's tests, they are compiled and linked, and run only by pushing `test.kexe` by hand. Next: a namespace that yields at least one binding, and a run task. -- **16.2 The product klib path under a Kotlin version skew — open.** `KlibScannerTest` reads klibs +- **16.2 The product klib path under a Kotlin version skew, open.** `KlibScannerTest` reads klibs from Kotlin/Native 2.0.20 in-process; reading this build's 2.4.20-Beta2 klibs in-process fails with `InvalidProtocolBufferException`. The worker path with a per-build reader classpath exists to avoid that, and nothing asserts it does. -- **16.3 `aar` files and project class directories are skipped silently — open.** Only `.jar` and +- **16.3 `aar` files and project class directories are skipped silently, open.** Only `.jar` and `.klib` are walked (`GP/main/.../artifact/PythonArtifactBindingsTask.kt` KDoc and filter). -- **16.4 Member (instance) functions of jar classes are not bound — open.** The scanner drops +- **16.4 Member (instance) functions of jar classes are not bound, open.** The scanner drops non-`static` methods: "an instance method needs a receiver" (`ArtifactScanner.kt` KDoc table). -- **16.5 A value-returning function slot (`() -> Float`) does not accept a Python callable — open.** +- **16.5 A value-returning function slot (`() -> Float`) does not accept a Python callable, open.** SPEC B-5; pinned red-by-design by `M3ProofRenderTest.aValueReturningFunctionSlotDoesNotYetAcceptAPythonCallable`. -- **16.6 `.pyi` stubs for handle-returning functions are not wrapped — open.** SPEC B-7. -- **16.7 Compose through Python on Android, iOS and wasm — open.** SPEC B-6/N-2: the render proofs - (`ksp-fixtures/compose/src/desktopTest/`) are desktop only. -- **16.8 Two material3 components not reachable from Python — open.** The dynamic `ColorScheme` +- **16.6 `.pyi` stubs for handle-returning functions are not wrapped, open.** SPEC B-7. +- **16.7 Compose through Python on Android, iOS and wasm, open.** SPEC B-6/N-2: the render proofs + (`python-multiplatform-compose/fixtures/compose/src/desktopTest/`) are desktop only. +- **16.8 Two material3 components not reachable from Python, open.** The dynamic `ColorScheme` factory is Android-only. Since #73 the class name resolves to `ColorScheme`'s own constructor (its `Color` parameters no longer hide it), which needs every colour written out; nothing renders with one yet (`M3ProofRenderTest.colorSchemeResolvesToItsConstructorAndABareCallNamesItsOverloads`). `DropdownMenu` @@ -371,24 +371,24 @@ description, §16e/§16f's open remainder is below. ## 17. Owed to the ecosystem, and decisions waiting on the maintainer -From `docs/design/ecosystem.md` §5, "What each repository owes — PythonMultiplatform". Of its gaps, +From `docs/design/ecosystem.md` §5, "What each repository owes, PythonMultiplatform". Of its gaps, the `@Composable` callable shape, the `sys.meta_path` finder (`_Finder` in `PM/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapter.kt`, tested in `PythonxAdapterTest.kt`), the `Composer` hand-off and Python-callable lifetime across recompositions -(`ksp-fixtures/compose/src/desktopTest/.../ComposableRenderTest.kt`, `RecompositionAccumulationTest.kt`) +(`python-multiplatform-compose/fixtures/compose/src/desktopTest/.../ComposableRenderTest.kt`, `RecompositionAccumulationTest.kt`) are done on desktop; the other platforms are 16.7. The production-bundler question is 10.4. -- **17.1 Retire `stagePythonHome` in favour of pypackpack's Python distribution — open (cross-repo).** +- **17.1 Retire `stagePythonHome` in favour of pypackpack's Python distribution, open (cross-repo).** Two acquisition paths disagree on source and destination (`ecosystem.md` §4 item 4). The work lands in pypackpack first; this repository then removes `PythonHomeStaging.kt`. Needs the maintainer's go (AGENTS.md §7). -- **17.2 Hand `stageWasmBrowserRuntime` to toolchain — open (cross-repo).** Same shape as 17.1, for the +- **17.2 Hand `stageWasmBrowserRuntime` to toolchain, open (cross-repo).** Same shape as 17.1, for the wasm runtime staging (`WasmBrowserRuntimeStaging.kt`). -- **17.3 Where the generic `pythonx` adapter machinery belongs — decision.** It lives in +- **17.3 Where the generic `pythonx` adapter machinery belongs, decision.** It lives in `PM/commonMain/kotlin/python/multiplatform/ffi/pythonx/`, while INTENT §2.3 puts `pythonx` in pythonx-compose (SPEC "Outside intent" item 4). It no longer synthesises a `pythonx` module (it installs `python_multiplatform.binding`); whether it stays here as a service is unstated. -- **17.4 The committed CPython 3.13 trees — decision.** `python-multiplatform/src/nativeInterop/cinterop/lib` - (15 555 tracked files, Android/desktop/iOS) and `binary/` (3.13.0 archives) are read only by the - `-PpythonVersion=3.13.0` desktop branch; the Android 3.13 stdlib trees by nothing. INTENT §3 says the +- **17.4 The committed CPython 3.13 trees, decision.** `python-multiplatform/src/nativeInterop/cinterop/lib` + (15 555 tracked files, Android/desktop/iOS) is read only by the + `-PpythonVersion=3.13.0` desktop branch (`binary/`, unreferenced 3.13.0 archives, was removed on 2026-10-04); the Android 3.13 stdlib trees by nothing. INTENT §3 says the interpreter is not vendored. Removing them is a repository-size decision (`ecosystem.md` §5). diff --git a/gradle.properties b/gradle.properties index a0b3f7c09..c115670bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,9 +23,9 @@ org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.uikit.enabled=true # Python Build -pythonVersion=3.14.7 +pythonVersion=3.15.0 pythonFreeThreaded=false -pythonBuildStandaloneRelease=20260807 +pythonBuildStandaloneRelease=20261013 pythonAppleSupportBuild=b11 # GraalVM native-image (sample's nativeCompile task). Liberica NIK is not in a location Gradle's @@ -34,3 +34,12 @@ pythonAppleSupportBuild=b11 # toolchain any existing task resolves to. See AGENTS.md §19 for why tooling lives # under /Volumes/macMini rather than the internal disk. org.gradle.java.installations.paths=/Volumes/macMini/toolchains/bellsoft-liberica-vm-full-openjdk25-25.0.4/Contents/Home + +# compose-multiplatform-core-extended (#191, #249): the Compose modules a native-image build takes from +# the fork instead of upstream, and where from. Read in one place, by the notebook E2E's native image; +# the fork's coordinates and version line are changing (to 1.11.1-ext.N), and only these lines follow. +composeExtended.version=1.11.1001 +# The group the fork publishes under, as a prefix of upstream's (the fork moves to org.thisisthepy.compose). +composeExtended.groupPrefix=org.jetbrains.compose +composeExtended.modules=org.jetbrains.compose.ui:ui,org.jetbrains.compose.ui:ui-desktop,org.jetbrains.compose.ui:ui-text,org.jetbrains.compose.ui:ui-text-desktop,org.jetbrains.compose.foundation:foundation,org.jetbrains.compose.foundation:foundation-desktop +composeExtended.mavenLocal=true diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig deleted file mode 100644 index 17b5a8b76..000000000 --- a/iosApp/Configuration/Config.xcconfig +++ /dev/null @@ -1,3 +0,0 @@ -TEAM_ID= -BUNDLE_ID=org.thisisthepy.python.multiplatform.demo -APP_NAME=PythonDemo diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj deleted file mode 100644 index 7f0dbc4af..000000000 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,436 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; - 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; - 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; - 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; - 8B3F1A512CC49F1D00B85330 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B3F1A502CC49F1D00B85330 /* Python.xcframework */; }; - 8B3F1A5C2CC574B200B85330 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B3F1A502CC49F1D00B85330 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 8B3F1A4F2CC498EB00B85330 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 8B3F1A5C2CC574B200B85330 /* Python.xcframework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; - 7555FF7B242A565900829871 /* PythonDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PythonDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 8B3F1A502CC49F1D00B85330 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Python.xcframework; path = "../sample/build/xcode-frameworks/Python.xcframework"; sourceTree = ""; }; - 8B3F1A5A2CC4A49700B85330 /* dylib-Info-template.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "dylib-Info-template.plist"; sourceTree = ""; }; - AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B92378962B6B1156000C7307 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8B3F1A512CC49F1D00B85330 /* Python.xcframework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 058557D7273AAEEB004C7B11 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 42799AB246E5F90AF97AA0EF /* Frameworks */ = { - isa = PBXGroup; - children = ( - 8B3F1A502CC49F1D00B85330 /* Python.xcframework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 7555FF72242A565900829871 = { - isa = PBXGroup; - children = ( - AB1DB47929225F7C00F7AF9C /* Configuration */, - 7555FF7D242A565900829871 /* iosApp */, - 7555FF7C242A565900829871 /* Products */, - 42799AB246E5F90AF97AA0EF /* Frameworks */, - ); - sourceTree = ""; - }; - 7555FF7C242A565900829871 /* Products */ = { - isa = PBXGroup; - children = ( - 7555FF7B242A565900829871 /* PythonDemo.app */, - ); - name = Products; - sourceTree = ""; - }; - 7555FF7D242A565900829871 /* iosApp */ = { - isa = PBXGroup; - children = ( - 058557BA273AAA24004C7B11 /* Assets.xcassets */, - 7555FF82242A565900829871 /* ContentView.swift */, - 7555FF8C242A565B00829871 /* Info.plist */, - 2152FB032600AC8F00CF470E /* iOSApp.swift */, - 058557D7273AAEEB004C7B11 /* Preview Content */, - 8B3F1A5A2CC4A49700B85330 /* dylib-Info-template.plist */, - ); - path = iosApp; - sourceTree = ""; - }; - AB1DB47929225F7C00F7AF9C /* Configuration */ = { - isa = PBXGroup; - children = ( - AB3632DC29227652001CCB65 /* Config.xcconfig */, - ); - path = Configuration; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 7555FF7A242A565900829871 /* iosApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; - buildPhases = ( - F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, - 7555FF77242A565900829871 /* Sources */, - B92378962B6B1156000C7307 /* Frameworks */, - 7555FF79242A565900829871 /* Resources */, - 8B3F1A532CC49F9C00B85330 /* Install Python Standard Library and Payload */, - 8B3F1A4F2CC498EB00B85330 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = iosApp; - packageProductDependencies = ( - ); - productName = iosApp; - productReference = 7555FF7B242A565900829871 /* PythonDemo.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 7555FF73242A565900829871 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1600; - ORGANIZATIONNAME = orgName; - TargetAttributes = { - 7555FF7A242A565900829871 = { - CreatedOnToolsVersion = 11.3.1; - }; - }; - }; - buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 7555FF72242A565900829871; - packageReferences = ( - ); - productRefGroup = 7555FF7C242A565900829871 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 7555FF7A242A565900829871 /* iosApp */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 7555FF79242A565900829871 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, - 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 8B3F1A532CC49F9C00B85330 /* Install Python Standard Library and Payload */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Install Python Standard Library and Payload"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "set -e\n# SPEC L-10 / issue #59: the CPython stdlib and this app's python/ payload into the .app, with\n# extension modules wrapped as frameworks. See docs/platforms/ios-app-bundle.md.\ncd \"$SRCROOT/..\"\nexport PYTHON_PAYLOAD_DIR=\"$SRCROOT/../sample/python/src/main\"\n/bin/bash tools/xcode/install-python.sh\n"; - }; - F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Compile Kotlin Framework"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :python-multiplatform:embedAndSignAppleFrameworkForXcode\n./gradlew :sample:embedAndSignAppleFrameworkForXcode\n\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 7555FF77242A565900829871 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, - 7555FF83242A565900829871 /* ContentView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 7555FFA3242A565B00829871 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 7555FFA4242A565B00829871 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 7555FFA6242A565B00829871 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = ZP73KJVT6Q; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", - ); - INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - PRODUCT_NAME = "${APP_NAME}"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 7555FFA7242A565B00829871 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = ZP73KJVT6Q; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", - ); - INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - PRODUCT_NAME = "${APP_NAME}"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7555FFA3242A565B00829871 /* Debug */, - 7555FFA4242A565B00829871 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7555FFA6242A565B00829871 /* Debug */, - 7555FFA7242A565B00829871 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 7555FF73242A565900829871 /* Project object */; -} diff --git a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme deleted file mode 100644 index 946f638dd..000000000 --- a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index ee7e3ca03..000000000 --- a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 8edf56e7a..000000000 --- a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "images" : [ - { - "filename" : "app-icon-1024.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c5350..000000000 --- a/iosApp/iosApp/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift deleted file mode 100644 index 3653dcb62..000000000 --- a/iosApp/iosApp/ContentView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import UIKit -import SwiftUI -import Demo - - -struct ComposeView: UIViewControllerRepresentable { - func makeUIViewController(context: Context) -> UIViewController { - MainViewControllerKt.MainViewController() - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} -} - -struct ContentView: View { - var body: some View { - ComposeView() - .ignoresSafeArea(.keyboard) // Compose has own keyboard handler - } -} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist deleted file mode 100644 index 412e37812..000000000 --- a/iosApp/iosApp/Info.plist +++ /dev/null @@ -1,50 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - - UILaunchScreen - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c5350..000000000 --- a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/iosApp/iosApp/dylib-Info-template.plist b/iosApp/iosApp/dylib-Info-template.plist deleted file mode 100644 index f652e272f..000000000 --- a/iosApp/iosApp/dylib-Info-template.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - - CFBundleIdentifier - - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSupportedPlatforms - - iPhoneOS - - MinimumOSVersion - 12.0 - CFBundleVersion - 1 - - diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift deleted file mode 100644 index d83dca611..000000000 --- a/iosApp/iosApp/iOSApp.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -@main -struct iOSApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} \ No newline at end of file diff --git a/ksp-fixtures/klib-artifact/build.gradle.kts b/ksp-fixtures/klib-artifact/build.gradle.kts deleted file mode 100644 index cfb76ef3a..000000000 --- a/ksp-fixtures/klib-artifact/build.gradle.kts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * The artefact walker's klib half, end to end: a real third-party `.klib` the build resolves, - * walked at build time by `KlibScanner`, installed into the same `UpcallTable` KSP's own fragments - * go into. - * - * `:ksp-fixtures:artifact` is `docs/design/ecosystem.md` §5b's second producer over a **jar** -- this is - * the same producer over a **klib**, ROADMAP §16e's "investigated, not implemented" half. One target - * only, and it is `androidNativeArm64` rather than `desktop`: a jar is what a JVM target resolves, a - * klib is what a Kotlin/Native target resolves, and `PythonArtifactBindingsTask - * .artifactConfiguration`/`.artifactSourceSet` are single-valued (ROADMAP §16f #1) -- one module can - * wire one walked configuration, so this fixture is the klib producer's own module rather than a - * second target bolted onto `:artifact`. - * - * `kotlinx-coroutines-core` is the klib walked, for the same reason JUnit is `:artifact`'s: a real, - * independently-versioned third-party artefact nothing here controls or cooperates with. - * - * ### The suite is linked by the build and run by hand - * - * KGP gives `androidNativeArm64` a `linkDebugTestAndroidNativeArm64` and **no run task** -- it - * registers one only where it knows how to reach a host, and an Android device is neither the build - * machine nor `simctl`. `:python-multiplatform` solved that for itself with a hand-written - * `androidNativeArm64Test` task (device selection, `adb push --sync` of a 60 MB stdlib, a - * TeamCity-to-JUnit-XML parser) that is project-local. Until that is generalised, this module's - * suite runs like this, reusing the payload that task already stages on the device: - * - * ./gradlew :python-multiplatform:androidNativeArm64Test # once: stages lib/ and the stdlib - * ./gradlew :ksp-fixtures:klib-artifact:linkDebugTestAndroidNativeArm64 - * D=/data/local/tmp/pmp-nativetest-arm64-v8a - * adb push ksp-fixtures/klib-artifact/build/bin/androidNativeArm64/debugTest/test.kexe \ - * $D/klibtest.kexe - * adb shell "chmod 755 $D/klibtest.kexe && cd $D && \ - * LD_LIBRARY_PATH=$D/lib PYTHONHOME=$D ./klibtest.kexe --ktest_logger=TEAMCITY" - * - * **A green ordinary build therefore says these tests compile and link, not that they pass.** - */ -plugins { - alias(libs.plugins.kotlin.multiplatform) - id("io.github.thisisthepy.python.multiplatform.bindings") -} - -kotlin { - androidNativeArm64 { - // Mirrors `:ksp-fixtures:app`'s own androidNativeArm64 linker config: the test binary needs - // the same CPython symbols resolved at final link time, since `python-multiplatform`'s klib - // only declares them via cinterop and does not itself embed a static libpython. - val downloadDir = project(":python-multiplatform").layout.buildDirectory.dir("python-standalone").get().asFile - val pyVersion = project.findProperty("pythonVersion")?.toString() ?: project.rootProject.version.toString() - val libVersion = pyVersion.split('.').subList(0, 2).joinToString(".") - val targetExtractDir = "$downloadDir/extracted/$pyVersion/android-aarch64/prefix" - binaries.getTest(org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG).linkerOpts.addAll( - listOf("-L$targetExtractDir/lib/", "-lpython$libVersion", "-Wl,--allow-shlib-undefined"), - ) - } - - sourceSets { - val commonMain by getting { - dependencies { - implementation(projects.pythonMultiplatform) - } - } - val androidNativeArm64Main by getting { - dependencies { - // A real, separately-published klib. Nothing about it knows it is going to be - // walked -- KlibScanner reads its ABI the same way it would read anyone else's. - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") - } - } - val androidNativeArm64Test by getting { - dependencies { - implementation(libs.kotlin.test) - } - } - } -} - -pythonBindings { - role.set("app") - processor.set(projects.pythonMultiplatformKsp) - - artifactConfiguration.set("androidNativeArm64CompileKlibraries") - artifactSourceSet.set("androidNativeArm64Main") - // Broad on purpose, unlike `:artifact`'s narrow `junit.runner`/`junit.framework`: coroutines' - // public surface is almost entirely `CoroutineScope`/`Job`/`Flow`-typed extensions, plus a couple - // of `@PublishedApi internal` declarations that are binary-visible but not source-visible outside - // the library that owns them -- none of those are bindable today, so the whole namespace yields - // **zero** declarations. `WalkedKlibArtifactTableTest` pins that as the correct number, not a gap: - // an earlier version bound the `@PublishedApi internal` one and failed to compile the first time - // its output was actually built rather than only unit-tested -- `KlibScanWorkAction`'s KDoc has - // the story. See that file's own KDoc for what the walker declines and why. - artifactIncludePackages.set(listOf("kotlinx.coroutines")) -} diff --git a/python-checksums.properties b/python-checksums.properties index c7b46d0cf..806f0bf69 100644 --- a/python-checksums.properties +++ b/python-checksums.properties @@ -1,23 +1,23 @@ # Python Multiplatform Checksums android-aarch64-3.14.7=6d50cc3aa66e414a439594089bcdfb5f1264358155c70c1f00471c24cfb477fb -android-aarch64-3.15.0rc1=7d6fb28a69368841656f919a08141b0e3720837ac67987388037ee9d2a5c5492 +android-aarch64-3.15.0=571e4cf4e0aee3d1ab54a52ca3d4e6845b85ec0bb327f7debfb873172f445dab android-x86_64-3.14.7=2c16ce2359565cd8c24f86cfb75630768ba6607e732946b294b969797f583b60 -android-x86_64-3.15.0rc1=f048f5c60b538efc64e4c26399a0a21202ce430949d5b2c10369d624548a5dc3 +android-x86_64-3.15.0=2413d51948d612cf42baefba25295bb83c2ed84df5726af6fe5bde2dec5d87e7 ios-3.14-b11=b591f3301bd22a4f423c49c746cac9e55558b909fd14d6eb8327ccc62234ab7b -ios-3.15.0rc1-pythonorg=178bf7bef9cd0f18b27cacb98b14332a5ba44c9427543ffb71f8809bc1f11c3c +ios-3.15.0-pythonorg=68e98ab35cc0643ba3a227a8c7768da45c2a5e67dbe1bc26598537e96680fd19 linux-x86_64-3.14.7-20260807=a2478d654ed51d443bae21ec20ad927f116b4f5aae4094ab74918a6aa38f0575 linux-x86_64-3.14.7-20260807-freethreaded=13f80b7fc242814c0be4e2a4d8283f39c7e1c0f3f0d8f6d6a15cedb5b62eb6d3 -linux-x86_64-3.15.0rc1-20260807=aeb0d2cfe5e7028170039a95b96d74aa6830efe2fc49808122aa8f835d6e9f31 -linux-x86_64-3.15.0rc1-20260807-freethreaded=c04c940dbf8eca39c66c73d8c51330341a5a7f9b3b13f689ec14521c6357a44d +linux-x86_64-3.15.0-20261013=69740dcd9b3480b3d53760f2d3b75582adb14fa54434628dea32480ddede542a +linux-x86_64-3.15.0-20261013-freethreaded=afd020a480ce1767d6ac1b56e97b59c6c06f85e3a95a42f69ae146d25a07689e macos-aarch64-3.14.7-20260807=532645be202d3df8510ab318ca5faed92060a694c8327144e14fa81f31af0f6d macos-aarch64-3.14.7-20260807-freethreaded=618fe8d34b0704ce4a8aa2cfa6fa96063d6aa48970a131f19e1591d9785d1111 -macos-aarch64-3.15.0rc1-20260807=bfd0fa8be7d7dfb5c75f6848757feb4bb9e4274da634e97e77aa00c8e521a6a0 -macos-aarch64-3.15.0rc1-20260807-freethreaded=7dc7e3758b6a77358b24d795e0a7eee3b5f3d9bc5fa0e3e3c65db52898f07815 +macos-aarch64-3.15.0-20261013=b4ab2009803cef142e4ebf2ebef225f6875f83cc35caf4eefb437aa9b2b76783 +macos-aarch64-3.15.0-20261013-freethreaded=94e34d776cff596017d15fce1b2175d463628a765ddb37412a39aaba6d31bf67 macos-x86_64-3.14.7-20260807=9964e2d618ebea03be8ea3e65ab0ecc0f2b030ce203345b8f92654641fd4de66 macos-x86_64-3.14.7-20260807-freethreaded=746207f0d2644c5c98619f9d0ed37824d9871ea8d27640edc779a46d85038b3b -macos-x86_64-3.15.0rc1-20260807=f36c0921d03468747ed74c434e4b04749a35593718cd641d0f08fa01e9c7885e -macos-x86_64-3.15.0rc1-20260807-freethreaded=69beed4659c78b3683f1ea56c12956c7234f16e0417edf24f7f69c5a9f7b09ec +macos-x86_64-3.15.0-20261013=4760e8fb2821441c0dbc72be49a25400c6ae90be4daa7e2a49617ed09f6a2919 +macos-x86_64-3.15.0-20261013-freethreaded=c552d4f10738f35de58be64716ec5104f55337d71e962e2beb09164e98cff1ec windows-x86_64-3.14.7-20260807=840e368318d1630bd8efbd3f7de8fab32cb4bbd735baef733f9034f2676c6a92 windows-x86_64-3.14.7-20260807-freethreaded=c8323293c9e76e8b244a0e533894512f067dac2a6aa2dfcb3494cbe74b3af58e -windows-x86_64-3.15.0rc1-20260807=bc0d0f3c7b88873688f30bc78da8387a6653a5493424951d3a84c658c973198e -windows-x86_64-3.15.0rc1-20260807-freethreaded=e5998e014f16074ac9f6d61eb2a31ff8ac8bc331b61da2c5acf7d15abf4a2650 +windows-x86_64-3.15.0-20261013=743d2269e74a98f7c4281dbc2e8710c171be1965693720aac818ff0157e4d965 +windows-x86_64-3.15.0-20261013-freethreaded=aadcd190365f1154854478da313219691ab14cc35497daec66a2149f3571ae36 diff --git a/python-multiplatform-compose/build.gradle.kts b/python-multiplatform-compose/build.gradle.kts index bee7b9502..011f95798 100644 --- a/python-multiplatform-compose/build.gradle.kts +++ b/python-multiplatform-compose/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget /** * The Compose host for python-multiplatform: the `@Composable` entry point that draws a Python-declared - * root (`PythonContent`) and the composer/callable plumbing it shares with every Python call into a + * root (`PythonAppView`, `PythonWidget`, `PythonLauncher`) and the composer/callable plumbing it shares with every Python call into a * composable. * * A separate module so that `:python-multiplatform` itself never depends on Compose (same naming as @@ -35,6 +35,8 @@ kotlin { commonMain.dependencies { api(projects.pythonMultiplatform) api(compose.runtime) + api(compose.ui) + api(compose.runtimeSaveable) } } } diff --git a/ksp-fixtures/compose/build.gradle.kts b/python-multiplatform-compose/fixtures/compose/build.gradle.kts similarity index 89% rename from ksp-fixtures/compose/build.gradle.kts rename to python-multiplatform-compose/fixtures/compose/build.gradle.kts index 9a111a31f..b953f711e 100644 --- a/ksp-fixtures/compose/build.gradle.kts +++ b/python-multiplatform-compose/fixtures/compose/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget * The other end of `docs/archive/pythonx-adapter-design.md` §5: a real `@Composable`, called from Python, * inside a real composition, drawing a real pixel. * - * ### Why this is not `:ksp-fixtures:artifact` + * ### Why this is not `:python-multiplatform-gradle-plugin-fixtures:artifact` * * That module takes Compose as **plain Maven coordinates with no Compose plugin**, and its own * `build.gradle.kts` says why: what makes `Modifier.padding(16.dp)` reachable is the walker's gates @@ -69,7 +69,7 @@ pythonBindings { artifactConfiguration.set("desktopCompileClasspath") artifactSourceSet.set("desktopMain") - // Narrow on purpose, and narrower than `:ksp-fixtures:artifact`'s: binding all of Compose would + // Narrow on purpose, and narrower than `:python-multiplatform-gradle-plugin-fixtures:artifact`'s: binding all of Compose would // make this fixture's compile time the cost of a proof it does not need. Each entry earns its // place. // @@ -100,7 +100,7 @@ pythonBindings { // 6. Compose state, from Python (issue #38): `mutableStateOf` (a generic function, its `T` // read as `kotlin.Any?`) and the `value` property of `State`/`MutableState`. Classes, not // the package: `androidx.compose.runtime` is the whole runtime, and this is the one - // facade and two interfaces `PythonContentRenderTest` writes its root through. + // facade and two interfaces `PythonAppViewRenderTest` writes its root through. "androidx.compose.runtime.SnapshotStateKt", "androidx.compose.runtime.State", "androidx.compose.runtime.MutableState", @@ -111,16 +111,20 @@ pythonBindings { // `TextFieldStateRenderTest` is what each one is for. "androidx.compose.foundation.text.input.TextFieldState", "androidx.compose.foundation.text.input.TextFieldStateKt", + // 8. `rememberSaveableWrapper` (issue #174): the one function of `python-multiplatform-compose` + // Python calls inside a composition. Its file facade, not the package, so that the host + // composables (`PythonAppView` and friends) stay out of the table. + "python.multiplatform.compose.RememberSaveableKt", ), ) - // Stubs on: `generatePythonStubs` over the real Compose jars is what `tools/stubs/check-stubs.sh` (the mypy + // Stubs on: `generatePythonStubs` over the real Compose jars is what `.github/scripts/stubs/check-stubs.sh` (the mypy // check) and `.github/workflows/stubs.yml` (the published artifact) both run. Nothing depends on the // task, so the render tests do not pay for it. generateStubs.set(true) } /** - * The same JVM `:ksp-fixtures:artifact`'s tests need: JDK 21 with the preview FFM API, and a + * The same JVM `:python-multiplatform-gradle-plugin-fixtures:artifact`'s tests need: JDK 21 with the preview FFM API, and a * `java.library.path` for the `libpython` that `manager.loadLibPython` extracts next to the working * directory. */ diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/ModifierComparisons.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/ModifierComparisons.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/ModifierComparisons.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/ModifierComparisons.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonAnchoredDraggable.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonAnchoredDraggable.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonAnchoredDraggable.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonAnchoredDraggable.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposed.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposed.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposed.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposed.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposition.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposition.kt similarity index 96% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposition.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposition.kt index 28ce7a08e..564ad6fa6 100644 --- a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposition.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonComposition.kt @@ -16,9 +16,9 @@ import python.multiplatform.ffi.pythonx.PythonCallables * * The composer plumbing and the callable holder are no longer this fixture's: they are * `python-multiplatform-compose`'s `withPythonComposer` and `PythonCallableArena`, which the host - * entry point `PythonContent` is built on too. What stays is executing a *source string* per pass, + * entry point `PythonAppView` is built on too. What stays is executing a *source string* per pass, * which is a test harness rather than an entry point -- a host draws a Python-declared root with - * `PythonContent` (`PythonContentRenderTest`). + * `PythonAppView` (`PythonAppViewRenderTest`). * * ### Why exactly one, and not one per component * @@ -130,7 +130,7 @@ class CountingCallableArena : PythonCallableArena() { * nor `pythonx` can supply on their own: a `Modifier` chain has to start somewhere, and the thing it * starts from is `Modifier` the *expression* -- `androidx.compose.ui.Modifier.Companion`, an object * instance the walker cannot hand out because it only binds functions. - * `ksp-fixtures/artifact`'s `ComposeSeed.kt` solved the exact same gap the same way, for the + * `python-multiplatform-gradle-plugin/fixtures/artifact`'s `ComposeSeed.kt` solved the exact same gap the same way, for the * non-composable half of Compose's surface; this is that solution, in this module, so `Spacer` -- * whose one parameter has no default and is therefore not omittable -- has something to be given. * diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonContextMenuOpenDetector.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonContextMenuOpenDetector.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonContextMenuOpenDetector.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonContextMenuOpenDetector.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDragAndDropSource.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDragAndDropSource.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDragAndDropSource.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDragAndDropSource.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDraggable.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDraggable.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDraggable.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonDraggable.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonLayoutId.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonLayoutId.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonLayoutId.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonLayoutId.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonModifierLocal.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonModifierLocal.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonModifierLocal.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonModifierLocal.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPointerInput.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPointerInput.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPointerInput.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPointerInput.kt diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPullRefresh.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPullRefresh.kt similarity index 91% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPullRefresh.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPullRefresh.kt index d7f042280..642446801 100644 --- a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPullRefresh.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonPullRefresh.kt @@ -16,7 +16,7 @@ import python.multiplatform.ffi.PyObject import python.multiplatform.ffi.types.basic.PyFloat /** - * Test harness for `pullRefresh`'s callback overload — + * Test harness for `pullRefresh`'s callback overload, * `androidx.compose.material.pullrefresh.PullRefreshKt.pullRefresh(Modifier, * onPull: (Float) -> Float, onRelease: suspend (Float) -> Float, enabled: Boolean)`. * @@ -33,7 +33,7 @@ import python.multiplatform.ffi.types.basic.PyFloat * * `onRelease: suspend (Float) -> Float` is a suspend slot with a return value. The same shape * applies: Kotlin suspends, returns `0f`, and fires a one-way notification to Python. Python never - * suspends — the Kotlin lambda body is the only thing that suspends, exactly the way + * suspends, the Kotlin lambda body is the only thing that suspends, exactly the way * `pythonPointerInput`/`pythonDraggable` handle their own suspend slots. * * ### Why the scene is built here, not from a Python BODY string @@ -42,12 +42,12 @@ import python.multiplatform.ffi.types.basic.PyFloat * neither of which is in `artifactIncludePackages`. Building the nested-scroll tree in this * `@Composable` avoids walking those packages, keeping the scope of what the walker covers intact. * - * ### Infinite-animation trap — deliberately avoided + * ### Infinite-animation trap, deliberately avoided * * §10 of the design doc confirmed that a component that animates forever (`CircularProgressIndicator` * with no deterministic end state) prevents `ImageComposeScene.render()` from returning. * `PullRefreshIndicator` contains such an animation. **This harness does not use - * `PullRefreshIndicator`** — `pullRefresh` (the Modifier) and the indicator are separate APIs, + * `PullRefreshIndicator`**, `pullRefresh` (the Modifier) and the indicator are separate APIs, * and the callback-reaching proof needs only the former. * * ### Lifetime @@ -66,7 +66,7 @@ fun PythonPullRefreshHarness(onPull: PyObject, onRelease: PyObject, modifier: Mo .background(Color.White) .pullRefresh( onPull = { pullAvailable: Float -> - // Notify Python — one-way, return value stays in Kotlin. + // Notify Python, one-way, return value stays in Kotlin. // pullAvailable > 0 when dragging downward at the top of the scrollable child. val arg = PyFloat.from(pullAvailable.toDouble()) try { @@ -77,7 +77,7 @@ fun PythonPullRefreshHarness(onPull: PyObject, onRelease: PyObject, modifier: Mo pullAvailable // consume everything offered }, onRelease = { velocity: Float -> - // suspend slot — Kotlin suspends here; Python is a synchronous notification. + // suspend slot, Kotlin suspends here; Python is a synchronous notification. val arg = PyFloat.from(velocity.toDouble()) try { onRelease(arg).close() diff --git a/ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonSwipeable.kt b/python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonSwipeable.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonSwipeable.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopMain/kotlin/fixture/compose/PythonSwipeable.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnchoredDraggableRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnchoredDraggableRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnchoredDraggableRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnchoredDraggableRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnySlotScalarRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnySlotScalarRenderTest.kt similarity index 97% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnySlotScalarRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnySlotScalarRenderTest.kt index 80b873ad3..9071ee233 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnySlotScalarRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/AnySlotScalarRenderTest.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.unit.Density -import python.multiplatform.compose.PythonContent +import python.multiplatform.compose.PythonWidget import python.multiplatform.ffi.Python3 import python.multiplatform.ffi.pythonx.PythonxAdapter import python.multiplatform.ffi.upcall.PythonProxySource @@ -25,7 +25,7 @@ import kotlin.test.assertTrue * `Int`, and incrementing it from Python (`counter.value = counter.value + 1`, the walked * `MutableState.value` getter and setter) recomposes the root that read it. * - * Modelled on [PythonContentRenderTest]: `ImageComposeScene` has no frame clock, so the test is the + * Modelled on [PythonAppViewRenderTest]: `ImageComposeScene` has no frame clock, so the test is the * clock (`Snapshot.sendApplyNotifications()` and a render), and the root counts its own invocations so * a frame can be attributed. * @@ -81,7 +81,7 @@ class AnySlotScalarRenderTest { val root = Python3.import("__main__").getAttr("_asr_root") val scene = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { - PythonContent(root = root) + PythonWidget(root = root) } try { scene.render() diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/CallbackDrivenRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/CallbackDrivenRenderTest.kt similarity index 98% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/CallbackDrivenRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/CallbackDrivenRenderTest.kt index efd432946..e862498b1 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/CallbackDrivenRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/CallbackDrivenRenderTest.kt @@ -108,7 +108,7 @@ class CallbackDrivenRenderTest { val checked = pixelsOf(CHECKBOX) val moved = differing(unchecked, checked) println( - "compose input: Checkbox click -> onCheckedChange(True), " + + "compose input: Checkbox click -> on_checked_change(True), " + "ink ${inkOf(unchecked)} -> ${inkOf(checked)} px, $moved pixels changed", ) assertTrue( @@ -162,7 +162,7 @@ class CallbackDrivenRenderTest { val after = pixelsOf(SWITCH) val moved = differing(before, after) println( - "compose input: Switch click -> onCheckedChange(True), " + + "compose input: Switch click -> on_checked_change(True), " + "ink ${inkOf(before)} -> ${inkOf(after)} px, $moved pixels changed", ) // Deliberately not an ink comparison: the thumb slides from one end of the track to the @@ -272,7 +272,7 @@ class CallbackDrivenRenderTest { val typed = pixelsOf(TEXT_FIELD, width = FIELD_WIDTH, height = FIELD_HEIGHT) val moved = differing(empty, typed) println( - "compose input: TextField keystroke 'h' -> onValueChange, " + + "compose input: TextField keystroke 'h' -> on_value_change, " + "ink ${inkOf(empty)} -> ${inkOf(typed)} px, $moved pixels changed", ) assertTrue( @@ -445,12 +445,12 @@ class CallbackDrivenRenderTest { * show what the callback wrote. */ val CHECKBOX = """ from androidx.compose.material3 import Checkbox - Checkbox(_cb_state[0], onCheckedChange=_cb_toggle) + Checkbox(_cb_state[0], on_checked_change=_cb_toggle) """.trimIndent() val SWITCH = """ from androidx.compose.material3 import Switch - Switch(_cb_state[0], onCheckedChange=_cb_toggle) + Switch(_cb_state[0], on_checked_change=_cb_toggle) """.trimIndent() /** M3's `TextField` has no minimum-size default anywhere near `Checkbox`'s 48dp -- its own @@ -466,7 +466,7 @@ class CallbackDrivenRenderTest { * what makes a fresh scene show what the callback wrote. */ val TEXT_FIELD = """ from androidx.compose.material3 import TextField - TextField(_tf_state[0], onValueChange=_tf_on_change) + TextField(_tf_state[0], on_value_change=_tf_on_change) """.trimIndent() /** `java.awt.event.KeyEvent`'s constructor requires a non-null source `Component`; nothing diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ColorFactoryTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ColorFactoryTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ColorFactoryTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ColorFactoryTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt similarity index 98% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt index 2650f6007..ae014ac90 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt @@ -495,13 +495,13 @@ class ComposableRenderTest { val drawn = inkOf( """ from androidx.compose.material3 import Button, Text - Button(onClick=lambda: None, content=lambda: Text('hi')) + Button(on_click=lambda: None, content=lambda: Text('hi')) """.trimIndent(), ) val empty = inkOf( """ from androidx.compose.material3 import Button - Button(onClick=lambda: None, content=lambda: None) + Button(on_click=lambda: None, content=lambda: None) """.trimIndent(), ) println("compose render: Button(content=Text('hi')) -> $drawn px, empty content -> $empty px") @@ -565,11 +565,11 @@ class ComposableRenderTest { /** * **`ListItem`.** `headlineContent` is the one parameter with no default (also `Function2`, - * arity zero) and it is written under its **Kotlin** parameter name, `headlineContent` -- so - * this also pins that a keyword argument reaches a real multi-word parameter by its Kotlin name, - * not just the single-word ones (`content`, `modifier`) every other test here happens to use. - * (It used to be `headline_content`: the binder snake_cased parameters, which it no longer does; - * a Pythonic spelling is pythonx-compose's to define.) + * arity zero) and it is written under its **snake_cased** keyword, `headline_content` -- so + * this also pins that a keyword argument reaches a real multi-word parameter by its Pythonic name + * (issue #131), not just the single-word ones (`content`, `modifier`) every other test here + * happens to use. The Kotlin spelling `headlineContent=` reaches the same parameter; the empty + * control below is written that way, so both spellings render here. * * The empty control is not zero, unlike `Column`'s or `Row`'s: `ListItem`, like `Button`, always * paints its own container surface (`colors.containerColor` at `tonalElevation`) whether or not @@ -578,11 +578,11 @@ class ComposableRenderTest { * `Column`'s does. */ @Test - fun listItemComposesItsHeadlineContentUnderItsKotlinName() { + fun listItemComposesItsHeadlineContentUnderItsSnakeCasedName() { val drawnPixels = pixelsOf( """ from androidx.compose.material3 import ListItem, Text - ListItem(headlineContent=lambda: Text('hi')) + ListItem(headline_content=lambda: Text('hi')) """.trimIndent(), ) val emptyPixels = pixelsOf( @@ -600,7 +600,7 @@ class ComposableRenderTest { val drawnColors = drawnPixels.filter { it != BACKGROUND }.toSet() val emptyColors = emptyPixels.filter { it != BACKGROUND }.toSet() println( - "compose render: ListItem(headlineContent=Text('hi')) -> ${drawnColors.size} distinct colors, " + + "compose render: ListItem(headline_content=Text('hi')) -> ${drawnColors.size} distinct colors, " + "empty -> ${emptyColors.size} distinct colors", ) assertTrue(emptyColors.isNotEmpty(), "a ListItem with no headline must still draw its own container surface") diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposedRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposedRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposedRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposedRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ContextMenuOpenDetectorRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ContextMenuOpenDetectorRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ContextMenuOpenDetectorRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ContextMenuOpenDetectorRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/DragAndDropSourceRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/DragAndDropSourceRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/DragAndDropSourceRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/DragAndDropSourceRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableLeakTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableLeakTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableLeakTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableLeakTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/DraggableRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/IconRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/IconRenderTest.kt similarity index 99% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/IconRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/IconRenderTest.kt index ea984fb04..ec5c8156c 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/IconRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/IconRenderTest.kt @@ -112,13 +112,13 @@ class IconRenderTest { fun pythonLoadsAPainterAndIconDrawsIt() { val drawn = inkOf( """ - from androidx.compose.ui.res import painterResource + from androidx.compose.ui.res import painter_resource from androidx.compose.material3 import Icon - Icon(painterResource('$RESOURCE'), contentDescription='a square') + Icon(painter_resource('$RESOURCE'), content_description='a square') """.trimIndent(), ) val blank = inkOf("pass") - println("compose render: Icon(painterResource('$RESOURCE')) -> $drawn px, empty body -> $blank px") + println("compose render: Icon(painter_resource('$RESOURCE')) -> $drawn px, empty body -> $blank px") assertEquals(0, blank, "an empty composition must draw nothing, or the measurement is not measuring") assertEquals(SQUARE, drawn, "Icon did not draw the 24x24 square the resource holds") } diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/KotlinSignatureMetadataTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/KotlinSignatureMetadataTest.kt similarity index 89% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/KotlinSignatureMetadataTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/KotlinSignatureMetadataTest.kt index ee2b4f920..589f85712 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/KotlinSignatureMetadataTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/KotlinSignatureMetadataTest.kt @@ -17,7 +17,7 @@ import kotlin.test.assertTrue * * pythonx-compose re-exposes Kotlin-named modules Pythonically by **one rule** applied to every * module it lists, with no per-widget wrapper. That rule needs, for each function: its Kotlin - * parameter names, which have defaults, the overload set behind a base name, which parameters are + * parameter names (and, since issue #131, the Pythonic keyword the binder serves for each), which have defaults, the overload set behind a base name, which parameters are * value classes, and which one is the trailing `@Composable` content lambda. This pins that the * binder publishes exactly that -- through `inspect.signature` and `python_multiplatform.describe` * (`KotlinSurface`'s KDoc is the contract) -- for a real composable (`Checkbox`) and a real @@ -39,7 +39,9 @@ class KotlinSignatureMetadataTest { } /** - * `Checkbox(checked, onCheckedChange, modifier = Modifier, ...)`, as Kotlin declares it. Compose 1.11 + * `Checkbox(checked, onCheckedChange, modifier = Modifier, ...)`, as Kotlin declares it -- and as + * `inspect.signature` shows it to Python since issue #131: `on_checked_change`, the keyword a + * Python caller writes, while `describe` keeps the Kotlin name beside it. Compose 1.11 * has two `Checkbox` overloads (the second adds `checkmarkStroke`/`outlineStroke`), so the base name * answers `(*args, **kwargs)` and the signature lives on the specific overload callable. */ @@ -75,13 +77,14 @@ class KotlinSignatureMetadataTest { 'name': _ksm_d[0]['name'], 'composable': _ksm_d[0]['composable'], 'callback': _ksm_d[0]['parameters'][1]['type'].startswith('kotlin.Function1'), - 'described': [_q['name'] for _q in _ksm_d[0]['parameters']] == + 'described': [_q['python_name'] for _q in _ksm_d[0]['parameters']] == [_p.name for _p in _ksm_params], + 'kotlin_names': [_q['name'] for _q in _ksm_d[0]['parameters']][:2], } """.trimIndent(), ) assertEquals( - "[('checked', False, 'kotlin.Boolean'), ('onCheckedChange', False, " + + "[('checked', False, 'kotlin.Boolean'), ('on_checked_change', False, " + "${eval("repr(_ksm_d[0]['parameters'][1]['type'])")}), " + "('modifier', True, 'androidx.compose.ui.Modifier')]", eval("repr(_ksm['first'])"), @@ -103,7 +106,8 @@ class KotlinSignatureMetadataTest { ) assertEquals("True", eval("_ksm['composable']"), order) assertEquals("True", eval("_ksm['callback']"), order) - assertEquals("True", eval("_ksm['described']"), "$order: describe and inspect.signature must agree") + assertEquals("True", eval("_ksm['described']"), "$order: describe's python_name and inspect.signature must agree") + assertEquals("['checked', 'onCheckedChange']", eval("repr(_ksm['kotlin_names'])"), "$order: describe keeps the Kotlin names") } } diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/LayoutIdRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/LayoutIdRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/LayoutIdRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/LayoutIdRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/M3ProofRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/M3ProofRenderTest.kt similarity index 93% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/M3ProofRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/M3ProofRenderTest.kt index 45967c713..5ee546467 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/M3ProofRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/M3ProofRenderTest.kt @@ -24,21 +24,21 @@ import kotlin.test.assertTrue * - A scene whose Python body calls the component has ink (non-background pixels > 0). * - A scene whose Python body is `pass` or an empty equivalent has none (or fewer). * - Where the component fills the entire scene with its own surface (FAB, TopAppBar, Scaffold, - * TabRow), distinct-colour counting is used instead of ink counting — the same approach - * [ComposableRenderTest.listItemComposesItsHeadlineContentUnderItsKotlinName] uses for ListItem. + * TabRow), distinct-colour counting is used instead of ink counting, the same approach + * [ComposableRenderTest.listItemComposesItsHeadlineContentUnderItsSnakeCasedName] uses for ListItem. * * Components judged here (10 of 27): - * 1. HorizontalDivider — leaf, no required param - * 2. RadioButton — selected: Boolean, onClick: Function0 - * 3. LinearProgressIndicator — progress: () -> Float (determinate; the indeterminate overload + * 1. HorizontalDivider, leaf, no required param + * 2. RadioButton, selected: Boolean, onClick: Function0 + * 3. LinearProgressIndicator, progress: () -> Float (determinate; the indeterminate overload * animates forever and hangs the scene, see that test) - * 4. CircularProgressIndicator — progress: () -> Float, same reason - * 5. Surface — content: @Composable () -> Unit - * 6. Scaffold — content: @Composable (PaddingValues) -> Unit (fills scene — colour-set comparison) - * 7. Slider — value: Float, onValueChange: (Float) -> Unit (pixel-set comparison) - * 8. TopAppBar — title: @Composable () -> Unit (fills scene — colour-set comparison) - * 9. FloatingActionButton — onClick: Function0, content: @Composable () -> Unit (colour-set) - * 10. Tab + TabRow — selected/onClick/text inside TabRow (colour-set) + * 4. CircularProgressIndicator, progress: () -> Float, same reason + * 5. Surface, content: @Composable () -> Unit + * 6. Scaffold, content: @Composable (PaddingValues) -> Unit (fills scene, colour-set comparison) + * 7. Slider, value: Float, onValueChange: (Float) -> Unit (pixel-set comparison) + * 8. TopAppBar, title: @Composable () -> Unit (fills scene, colour-set comparison) + * 9. FloatingActionButton, onClick: Function0, content: @Composable () -> Unit (colour-set) + * 10. Tab + TabRow, selected/onClick/text inside TabRow (colour-set) */ class M3ProofRenderTest { @@ -59,7 +59,7 @@ class M3ProofRenderTest { // ── 1. HorizontalDivider ────────────────────────────────────────────────── /** - * HorizontalDivider — leaf, every parameter defaulted. Must draw a visible line on a 200x10 + * HorizontalDivider, leaf, every parameter defaulted. Must draw a visible line on a 200x10 * scene; pass must draw nothing. */ @Test @@ -80,7 +80,7 @@ class M3ProofRenderTest { // ── 2. RadioButton ─────────────────────────────────────────────────────── /** - * RadioButton — selected=True paints a filled ring, selected=False paints only a ring. + * RadioButton, selected=True paints a filled ring, selected=False paints only a ring. * Both must draw; and the filled variant (selected) must have more ink than the hollow one. */ @Test @@ -88,13 +88,13 @@ class M3ProofRenderTest { val selected = inkOf( """ from androidx.compose.material3 import RadioButton - RadioButton(selected=True, onClick=lambda: None) + RadioButton(selected=True, on_click=lambda: None) """.trimIndent(), ) val deselected = inkOf( """ from androidx.compose.material3 import RadioButton - RadioButton(selected=False, onClick=lambda: None) + RadioButton(selected=False, on_click=lambda: None) """.trimIndent(), ) println("compose render: RadioButton selected=$selected px, deselected=$deselected px") @@ -197,7 +197,7 @@ class M3ProofRenderTest { // ── 5. Surface ─────────────────────────────────────────────────────────── /** - * Surface — content: @Composable () -> Unit (Function2, arity 0, same shape as MaterialTheme). + * Surface, content: @Composable () -> Unit (Function2, arity 0, same shape as MaterialTheme). * Surface's own container background does not fill the full 200x60 scene at its default size, * so ink-counting works: with Text inside vs empty content gives different non-zero ink. */ @@ -223,7 +223,7 @@ class M3ProofRenderTest { // ── 6. Scaffold ────────────────────────────────────────────────────────── /** - * Scaffold — content: @Composable (PaddingValues) -> Unit. Scaffold fills the scene with its + * Scaffold, content: @Composable (PaddingValues) -> Unit. Scaffold fills the scene with its * own surface, so ink-counting cannot distinguish content from no content (both max out at * scene pixels). Distinct-colour counting (same approach as ListItem) is the right tool: the * glyph colour is not the container fill, so Text inside adds a new colour to the set. @@ -260,9 +260,9 @@ class M3ProofRenderTest { // ── 7. Slider ──────────────────────────────────────────────────────────── /** - * Slider — value: Float, onValueChange: (Float) -> Unit, both required. + * Slider, value: Float, onValueChange: (Float) -> Unit, both required. * Slider(0.0) places the thumb at the left end; Slider(1.0) at the right. The thumb is the - * only element that moves, so the two pixel sets must differ — same argument + * only element that moves, so the two pixel sets must differ, same argument * [ComposableRenderTest.theStringPythonWroteIsTheStringComposeDrew] uses for Text length. */ @Test @@ -270,14 +270,14 @@ class M3ProofRenderTest { val left = pixelsOf( """ from androidx.compose.material3 import Slider - Slider(0.0, onValueChange=lambda v: None) + Slider(0.0, on_value_change=lambda v: None) """.trimIndent(), width = 200, height = 48, ) val right = pixelsOf( """ from androidx.compose.material3 import Slider - Slider(1.0, onValueChange=lambda v: None) + Slider(1.0, on_value_change=lambda v: None) """.trimIndent(), width = 200, height = 48, ) @@ -286,14 +286,14 @@ class M3ProofRenderTest { assertTrue(ink > 0, "Slider drew nothing at value=0.0") assertTrue( !left.contentEquals(right), - "Slider(0.0) and Slider(1.0) produced identical pixels — the value argument was not read", + "Slider(0.0) and Slider(1.0) produced identical pixels, the value argument was not read", ) } // ── 8. TopAppBar ───────────────────────────────────────────────────────── /** - * TopAppBar — title: @Composable () -> Unit (Function2, arity 0), required. + * TopAppBar, title: @Composable () -> Unit (Function2, arity 0), required. * TopAppBar fills the full scene with its own container, so ink counting gives the same * result with or without a title. Distinct-colour counting finds the glyph colour. */ @@ -329,7 +329,7 @@ class M3ProofRenderTest { // ── 9. FloatingActionButton ─────────────────────────────────────────────── /** - * FloatingActionButton — onClick: Function0, content: @Composable () -> Unit, both required. + * FloatingActionButton, onClick: Function0, content: @Composable () -> Unit, both required. * The FAB always draws its own container surface, which in a 200x60 scene fills nearly the * full height. Distinct-colour counting finds the glyph colour added by Text inside. */ @@ -338,13 +338,13 @@ class M3ProofRenderTest { val drawnPixels = pixelsOf( """ from androidx.compose.material3 import FloatingActionButton, Text - FloatingActionButton(onClick=lambda: None, content=lambda: Text('hi')) + FloatingActionButton(on_click=lambda: None, content=lambda: Text('hi')) """.trimIndent(), ) val emptyPixels = pixelsOf( """ from androidx.compose.material3 import FloatingActionButton - FloatingActionButton(onClick=lambda: None, content=lambda: None) + FloatingActionButton(on_click=lambda: None, content=lambda: None) """.trimIndent(), ) val drawnColors = drawnPixels.filter { it != BACKGROUND }.toSet() @@ -363,8 +363,8 @@ class M3ProofRenderTest { // ── 10. Tab + TabRow ────────────────────────────────────────────────────── /** - * TabRow — selectedTabIndex: Int, tabs: @Composable () -> Unit. - * Tab — selected: Boolean, onClick: Function0, text: @Composable () -> Unit. + * TabRow, selectedTabIndex: Int, tabs: @Composable () -> Unit. + * Tab, selected: Boolean, onClick: Function0, text: @Composable () -> Unit. * * TabRow fills the scene with its indicator and background, so distinct-colour counting is * again the right tool. Tab's text slot carries the glyph colour. @@ -375,7 +375,7 @@ class M3ProofRenderTest { """ from androidx.compose.material3 import TabRow, Tab, Text TabRow( - selectedTabIndex=0, + selected_tab_index=0, tabs=lambda: Tab( selected=True, onClick=lambda: None, @@ -389,7 +389,7 @@ class M3ProofRenderTest { """ from androidx.compose.material3 import TabRow, Tab TabRow( - selectedTabIndex=0, + selected_tab_index=0, tabs=lambda: Tab( selected=True, onClick=lambda: None, @@ -419,13 +419,13 @@ class M3ProofRenderTest { val checked = inkOf( """ from androidx.compose.material3 import Checkbox - Checkbox(checked=True, onCheckedChange=lambda v: None) + Checkbox(checked=True, on_checked_change=lambda v: None) """.trimIndent(), ) val unchecked = inkOf( """ from androidx.compose.material3 import Checkbox - Checkbox(checked=False, onCheckedChange=lambda v: None) + Checkbox(checked=False, on_checked_change=lambda v: None) """.trimIndent(), ) println("compose render: Checkbox checked=$checked px, unchecked=$unchecked px") @@ -450,13 +450,13 @@ class M3ProofRenderTest { val checked = pixelsOf( """ from androidx.compose.material3 import Switch - Switch(checked=True, onCheckedChange=lambda v: None) + Switch(checked=True, on_checked_change=lambda v: None) """.trimIndent(), ) val unchecked = pixelsOf( """ from androidx.compose.material3 import Switch - Switch(checked=False, onCheckedChange=lambda v: None) + Switch(checked=False, on_checked_change=lambda v: None) """.trimIndent(), ) val checkedInk = checked.count { it != BACKGROUND } @@ -568,13 +568,13 @@ class M3ProofRenderTest { val drawnPixels = pixelsOf( """ from androidx.compose.material3 import ExtendedFloatingActionButton, Text - ExtendedFloatingActionButton(onClick=lambda: None, text=lambda: Text('hi'), icon=lambda: None) + ExtendedFloatingActionButton(on_click=lambda: None, text=lambda: Text('hi'), icon=lambda: None) """.trimIndent(), ) val emptyPixels = pixelsOf( """ from androidx.compose.material3 import ExtendedFloatingActionButton - ExtendedFloatingActionButton(onClick=lambda: None, text=lambda: None, icon=lambda: None) + ExtendedFloatingActionButton(on_click=lambda: None, text=lambda: None, icon=lambda: None) """.trimIndent(), ) val drawnColors = drawnPixels.filter { it != BACKGROUND }.toSet() diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/MaterialIconsRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/MaterialIconsRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/MaterialIconsRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/MaterialIconsRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/MemberResolverComposeTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/MemberResolverComposeTest.kt similarity index 51% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/MemberResolverComposeTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/MemberResolverComposeTest.kt index d02a3b03f..983d935d0 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/MemberResolverComposeTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/MemberResolverComposeTest.kt @@ -36,6 +36,12 @@ class MemberResolverComposeTest { UpcallTable.clear() } + /** + * A name only a resolver knows (`full_width`, which is not the snake_case of any Kotlin name) is an + * `AttributeError` until a resolver answers it, runs Compose's real `fillMaxWidth` while it is + * registered, and is an `AttributeError` again once it is removed. The binder's own alias + * (`fill_max_width`) needs no resolver (issue #131) and is covered by the next test. + */ @Test fun aResolvedSnakeNameRunsComposesFillMaxWidthInARealChain() { Python3.exec( @@ -47,32 +53,53 @@ class MemberResolverComposeTest { _start = padding__Dp(emptyModifier(), 16.0) try: - _start.fill_max_width - raise AssertionError('fill_max_width resolved with no resolver registered') + _start.full_width + raise AssertionError('full_width resolved with no resolver registered') except AttributeError: pass - def _snake_to_camel(type_name, requested, kotlin_names): - head, *rest = requested.split('_') - camel = head + ''.join(p.capitalize() for p in rest) - return camel if camel in kotlin_names else None + def _full_width(type_name, requested, kotlin_names): + return 'fillMaxWidth' if requested == 'full_width' and 'fillMaxWidth' in kotlin_names else None - _b.add_member_resolver(_snake_to_camel) + _b.add_member_resolver(_full_width) try: - _chain = _start.fill_max_width() + _chain = _start.full_width() assert equalsPaddingThenFillMaxWidth(_chain._pm_handle, 16.0), 'chain is not padding(16.dp).fillMaxWidth()' assert not equalsPaddingThenFillMaxWidth(_chain._pm_handle, 17.0), '16dp matched 17dp' assert not equalsFillMaxWidthThenPadding(_chain._pm_handle, 16.0), 'the chain order is not observed' - assert 'fill_max_width' not in dir(_chain), 'the alias leaked into dir()' - assert 'fill_max_width' not in vars(type(_chain)), 'the alias was written onto the proxy class' + assert 'full_width' not in dir(_chain), 'the alias leaked into dir()' + assert 'full_width' not in vars(type(_chain)), 'the alias was written onto the proxy class' finally: - _b.remove_member_resolver(_snake_to_camel) + _b.remove_member_resolver(_full_width) try: - _start.fill_max_width + _start.full_width raise AssertionError('the alias survived removing the resolver') except AttributeError: pass """.trimIndent(), ) } + + /** + * Issue #131 (SPEC U-12): with no resolver registered the binder serves `fill_max_width` itself, as + * the same declaration as `fillMaxWidth`; the alias is never listed and never written onto the class. + */ + @Test + fun theBindersOwnSnakeAliasRunsComposesFillMaxWidthWithNoResolver() { + Python3.exec( + """ + from androidx.compose.foundation.layout import padding__Dp + from fixture.compose import emptyModifier + from fixture.compose import equalsPaddingThenFillMaxWidth, equalsFillMaxWidthThenPadding + + _start = padding__Dp(emptyModifier(), 16.0) + _chain = _start.fill_max_width() + assert equalsPaddingThenFillMaxWidth(_chain._pm_handle, 16.0), 'chain is not padding(16.dp).fillMaxWidth()' + assert not equalsPaddingThenFillMaxWidth(_chain._pm_handle, 17.0), '16dp matched 17dp' + assert not equalsFillMaxWidthThenPadding(_chain._pm_handle, 16.0), 'the chain order is not observed' + assert 'fill_max_width' not in dir(_chain), 'the alias leaked into dir()' + assert 'fill_max_width' not in vars(type(_chain)), 'the alias was written onto the proxy class' + """.trimIndent(), + ) + } } diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierLocalRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierLocalRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierLocalRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierLocalRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierSeededRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierSeededRenderTest.kt similarity index 97% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierSeededRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierSeededRenderTest.kt index 263996e41..e2e21f9db 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierSeededRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ModifierSeededRenderTest.kt @@ -29,7 +29,7 @@ import kotlin.test.assertTrue * `Box__Modifier`, has the identical shape and is left equally unreached here). Filling that slot * needs a `Modifier` *value*, and nothing the artefact walker binds can produce one on its own: * `Modifier`'s only source is `androidx.compose.ui.Modifier.Companion`, an object instance, and - * `ArtifactScanner` binds functions, not statics of an arbitrary object. `ksp-fixtures/artifact`'s + * `ArtifactScanner` binds functions, not statics of an arbitrary object. `python-multiplatform-gradle-plugin/fixtures/artifact`'s * `ComposeSeed.kt` hit exactly this wall and solved it with one hand-written Kotlin function that * KSP binds instead; `fixture.compose.emptyModifier` (`PythonComposition.kt`) is that same seed, * placed in this module so `Spacer` has something to receive. @@ -40,7 +40,7 @@ import kotlin.test.assertTrue * reached through `pythonx` (`docs/archive/pythonx-adapter-design.md`'s dynamic loader is for the * composable half of the surface). They come from the *other* producer, * `PythonProxySource` + `FunctionTable`, the route `WalkedArtifactComposeModifierTest` uses for the - * identical seed in `ksp-fixtures/artifact`. Both producers run in the same test here -- + * identical seed in `python-multiplatform-gradle-plugin/fixtures/artifact`. Both producers run in the same test here -- * `PythonxAdapter.install(PYTHONX_RAW_VALUE_CLASSES)` for `Spacer` itself, `PythonProxySource.install()` for the `Modifier` * chain that fills its one argument -- kept out of `ComposableRenderTest`'s shared setup so a * regression in this newer combination cannot fail ten unrelated tests that never asked for it. diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ObjectConstantRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ObjectConstantRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ObjectConstantRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/ObjectConstantRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PointerInputRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PointerInputRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PointerInputRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PointerInputRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PullRefreshRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PullRefreshRenderTest.kt similarity index 94% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PullRefreshRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PullRefreshRenderTest.kt index 8f77187aa..b4db81b15 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PullRefreshRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PullRefreshRenderTest.kt @@ -26,7 +26,7 @@ import kotlin.test.assertTrue * .pullRefresh(Modifier, onPull: (Float) -> Float, onRelease: suspend (Float) -> Float, enabled: * Boolean)`) is driven by **nested scroll** from a scrollable descendant, not by pointer events * the modifier itself awaits. `docs/archive/pythonx-adapter-design.md` §9.4 identified this as needing - * a larger harness — a real scrollable descendant composed under the `pullRefresh` parent, with + * a larger harness, a real scrollable descendant composed under the `pullRefresh` parent, with * an actual drag that makes the scrollable child's nested-scroll callbacks flow upward. * * ### What is proven here @@ -42,7 +42,7 @@ import kotlin.test.assertTrue * * `onPull: (Float) -> Float` must return the amount consumed. §9.6 settled the shape for * value-returning slots: Kotlin supplies the return value, Python is given a one-way notification. - * `pythonPullRefreshCallbacks` implements exactly that — the lambda always returns `pullAvailable` + * `pythonPullRefreshCallbacks` implements exactly that, the lambda always returns `pullAvailable` * (consuming everything offered) while firing a notification to Python. * * `onRelease: suspend (Float) -> Float` is the same: Kotlin closes the coroutine returning `0f`, @@ -53,7 +53,7 @@ import kotlin.test.assertTrue * A scrollable descendant inside Compose requires `rememberScrollState()` and `verticalScroll`, * which are not in `artifactIncludePackages`. Building the nested-scroll parent/child tree in * Kotlin (inside `PythonPullRefreshHarness`) avoids adding those packages to the walker's scope - * — this proof is about the callback boundary, not about what the walker covers. + *, this proof is about the callback boundary, not about what the walker covers. */ class PullRefreshRenderTest { @@ -106,7 +106,7 @@ class PullRefreshRenderTest { } try { scene.render() - // Drag downward from near the top — the scrollable child is already at the top, so + // Drag downward from near the top, the scrollable child is already at the top, so // over-scroll flows into PullRefreshNestedScrollConnection.onPreScroll -> onPull. scene.sendPointerEvent(PointerEventType.Move, Offset(SCENE / 2f, START_Y), type = PointerType.Touch) scene.sendPointerEvent(PointerEventType.Press, Offset(SCENE / 2f, START_Y), type = PointerType.Touch) @@ -176,7 +176,7 @@ class PullRefreshRenderTest { } try { scene.render() - // Press and drag in the bottom-right corner — outside the Box that carries pullRefresh. + // Press and drag in the bottom-right corner, outside the Box that carries pullRefresh. val at = Offset(SCENE - 2f, SCENE - 2f) scene.sendPointerEvent(PointerEventType.Move, at, type = PointerType.Touch) scene.sendPointerEvent(PointerEventType.Press, at, type = PointerType.Touch) diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonContentRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonAppViewRenderTest.kt similarity index 81% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonContentRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonAppViewRenderTest.kt index 95bf6abd4..8c5e45aa8 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonContentRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonAppViewRenderTest.kt @@ -5,7 +5,9 @@ import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.unit.Density import org.jetbrains.skia.Bitmap import org.jetbrains.skia.Image -import python.multiplatform.compose.PythonContent +import python.multiplatform.compose.PythonAppView +import python.multiplatform.compose.PythonLauncher +import python.multiplatform.compose.PythonWidget import python.multiplatform.ffi.Python3 import python.multiplatform.ffi.pythonx.PythonCallables import python.multiplatform.ffi.pythonx.PythonxAdapter @@ -35,13 +37,13 @@ import kotlin.test.assertTrue * the next frame shows the new root, and nothing on the Kotlin side was called to make it so. The * only thing the test does between the write and the frame is what a window's frame clock does on * its own -- deliver the snapshot's apply notification -- and the per-root call counters show that the - * host's `PythonContent` was not re-entered by anything but that. + * host's `PythonAppView` was not re-entered by anything but that. * * The lifetime half follows `ComposableRenderTest.aDisposedCompositionGivesEveryPythonCallableBack`: * a named Python function crosses as a `content=` slot, its `sys.getrefcount` rises while composed, * and comes back to its baseline exactly -- above means a leak, below means a double release. */ -class PythonContentRenderTest { +class PythonAppViewRenderTest { @BeforeTest fun installBothProducers() { @@ -63,7 +65,7 @@ class PythonContentRenderTest { /** * Declare, render, replace by a state write, render, replace back, render, dispose. * - * Reached through `PythonContent(module, attribute)`, the spelling a host that knows only a module + * Reached through `PythonAppView(module, attribute)`, the spelling a host that knows only a module * name uses. */ @Test @@ -73,7 +75,7 @@ class PythonContentRenderTest { val baseInner = pyInt("sys.getrefcount(_inner)") val scene = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { - PythonContent(module = "__main__", attribute = "_root_state") + PythonAppView(module = "__main__", attribute = "_root_state") } val inkA: Int val inkB: Int @@ -130,7 +132,7 @@ class PythonContentRenderTest { val state = Python3.import("__main__").getAttr("_root_state") val scene = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { - PythonContent(root = state) + PythonWidget(root = state) } val ink: Int val held: Int @@ -191,7 +193,7 @@ class PythonContentRenderTest { fun aPlainPythonCallableIsAFixedRoot() { val root = Python3.import("__main__").getAttr("_root_a") val scene = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { - PythonContent(root = root) + PythonWidget(root = root) } val ink: Int try { @@ -204,6 +206,60 @@ class PythonContentRenderTest { assertTrue(ink > 0, "the callable root drew nothing") } + /** `PythonWidget(name, moduleName = ...)` draws exactly what `PythonWidget(root = )` draws. */ + @Test + fun aNamedWidgetDrawsTheSamePixelsAsThePassedCallable() { + val root = Python3.import("__main__").getAttr("_root_a") + val byObject = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { + PythonWidget(root = root) + } + val inkObject: Int + try { + inkObject = inkOfImage(byObject.render()) + } finally { + byObject.close() + } + root.close() + val byName = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { + PythonWidget("_root_a", moduleName = "__main__") + } + val inkName: Int + try { + inkName = inkOfImage(byName.render()) + } finally { + byName.close() + } + assertTrue(inkObject > 0, "the callable drew nothing") + assertEquals(inkObject, inkName, "the named widget drew different pixels than the callable it names") + assertEquals(2, pyInt("_calls['a']"), "each of the two compositions should have called the root once") + } + + /** + * `PythonLauncher` composes its content. The interpreter is already initialised here (the + * `@BeforeTest` of this class, and earlier classes in the same JVM, did it), so what is tested is + * that the launcher leaves a running interpreter alone and draws `PythonAppView` inside it; the + * not-initialised start cannot be arranged safely, as Python cannot be re-initialised after a + * finalize. + */ + @Test + fun aLauncherComposesItsContent() { + Python3.exec("_root_state.value = _root_a") + val scene = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { + PythonLauncher { + PythonAppView(module = "__main__", attribute = "_root_state") + } + } + val ink: Int + try { + ink = inkOfImage(scene.render()) + } finally { + scene.close() + } + assertTrue(Python3.isInitialized) + assertTrue(ink > 0, "the content of the launcher drew nothing") + assertEquals(1, pyInt("_calls['a']")) + } + /** * What a window's frame clock does by itself: deliver the global snapshot's pending writes to the * recomposer, then draw. `ImageComposeScene` has no clock, so the test is the clock. diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonxModules.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonxModules.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonxModules.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/PythonxModules.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/RecompositionAccumulationTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/RecompositionAccumulationTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/RecompositionAccumulationTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/RecompositionAccumulationTest.kt diff --git a/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/RememberSaveableRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/RememberSaveableRenderTest.kt new file mode 100644 index 000000000..12ec71387 --- /dev/null +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/RememberSaveableRenderTest.kt @@ -0,0 +1,185 @@ +package fixture.compose + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.saveable.LocalSaveableStateRegistry +import androidx.compose.runtime.saveable.SaveableStateRegistry +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.unit.Density +import python.multiplatform.compose.PythonWidget +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.pythonx.PythonxAdapter +import python.multiplatform.ffi.upcall.UpcallBootstrap +import python.multiplatform.generated.artifacts.ArtifactTable +import python.multiplatform.reflection.UpcallTable +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * **`rememberSaveableWrapper` from Python** (issue #174): the original pycomposeui's + * `rememberSaveableWrapper(init, type)`, as `python.multiplatform.compose.rememberSaveableWrapper(initial)`, + * reached through the walk (`RememberSaveableKt`) and called inside a Python root. + * + * The root asks for one state per supported type (Int, Long, Double, Boolean, String), draws them, and + * keeps them in `_rs_states` so the test can write `.value` from Python. The scene is hosted inside + * `CompositionLocalProvider(LocalSaveableStateRegistry provides registry)`; the test writes, saves the + * registry, closes the scene, builds a new registry from the saved map and a new scene over the same + * root, and reads the values the new states hold. + * + * ### Red before the implementation + * + * `from python.multiplatform.compose import rememberSaveableWrapper` is an `ImportError` raised by the + * Python root when the first frame composes, so each test fails there and not at an assertion on a + * value, which is what tells that red from a regression. + */ +class RememberSaveableRenderTest { + + @BeforeTest + fun installProducers() { + Python3.initialize(silent = true) + UpcallTable.clear() + UpcallTable.install(ArtifactTable.fragments) + check(UpcallBootstrap.publishToGlobals()) { "UpcallBootstrap.publishToGlobals() failed" } + PythonxAdapter.install(PYTHONX_RAW_VALUE_CLASSES) + Python3.exec(ROOTS) + } + + @AfterTest + fun cleanup() { + Python3.exec( + """ + for _name in [n for n in globals() if n.startswith('_rs_')]: + del globals()[_name] + """.trimIndent(), + ) + UpcallTable.clear() + } + + /** Each supported type: initial value, written value, saved, restored into a new scene. */ + @Test + fun everySupportedTypeSurvivesASaveAndARestoreThroughTheRegistry() { + val saved = firstScene(canBeSaved = true) + println("rememberSaveableWrapper: saved ${saved.size} entries") + assertTrue(saved.isNotEmpty(), "nothing was saved, so the states are not registered with the registry") + + runScene(SaveableStateRegistry(restoredValues = saved, canBeSaved = { true })) + assertEquals(WRITTEN, snapshotOfStates(), "the restored states do not hold the values saved") + assertEquals( + listOf("int", "int", "float", "bool", "str"), + pyStr("[type(_rs_states[k].value).__name__ for k in 'ilfbs']").let { parseList(it) }, + ) + } + + /** + * The check that can fail: the same flow hosted without a registry (so nothing can be saved) shows + * the initial values again. + */ + @Test + fun withoutASaveableRegistryTheRecreatedSceneShowsTheInitialValues() { + val saved = firstScene(canBeSaved = null) + assertTrue(saved.isEmpty(), "a scene without a registry still saved $saved") + + runScene(null) + assertEquals(INITIAL, snapshotOfStates(), "values came back although nothing was saved") + } + + /** A value of any other type is an error that names the type. */ + @Test + fun anUnsupportedTypeRaisesAnErrorNamingTheType() { + val scene = ImageComposeScene(width = 200, height = 60, density = Density(1f)) { + CompositionLocalProvider(LocalSaveableStateRegistry provides SaveableStateRegistry(null) { true }) { + PythonWidget("_rs_root_bad", moduleName = "__main__") + } + } + try { + scene.render() + } finally { + scene.close() + } + val message = pyStr("_rs_error[0]") + println("rememberSaveableWrapper unsupported: $message") + assertTrue("list" in message, "the error does not name the Python list type: $message") + } + + private fun firstScene(canBeSaved: Boolean?): Map> { + val registry = canBeSaved?.let { SaveableStateRegistry(restoredValues = null, canBeSaved = { _ -> it }) } + val scene = host(registry) + try { + scene.render() + assertEquals(INITIAL, snapshotOfStates(), "the states did not start at the values Python passed") + Python3.exec("_rs_states['i'].value = 8; _rs_states['l'].value = 6000000000; _rs_states['f'].value = 2.5; _rs_states['b'].value = False; _rs_states['s'].value = 'xyz'") + Snapshot.sendApplyNotifications() + scene.render() + assertEquals(WRITTEN, snapshotOfStates(), "the write from Python was not read back") + return registry?.performSave() ?: emptyMap() + } finally { + scene.close() + } + } + + private fun runScene(registry: SaveableStateRegistry?) { + Python3.exec("_rs_states.clear()") + val scene = host(registry) + try { + scene.render() + } finally { + scene.close() + } + } + + private fun host(registry: SaveableStateRegistry?) = + ImageComposeScene(width = 300, height = 200, density = Density(1f)) { + if (registry == null) { + PythonWidget("_rs_root", moduleName = "__main__") + } else { + CompositionLocalProvider(LocalSaveableStateRegistry provides registry) { + PythonWidget("_rs_root", moduleName = "__main__") + } + } + } + + private fun snapshotOfStates(): String = pyStr("[_rs_states[k].value for k in 'ilfbs']") + + private fun parseList(text: String): List = + text.removePrefix("[").removeSuffix("]").split(", ").map { it.trim('\'') } + + private fun pyStr(expression: String): String = Python3.import("__main__").getAttr("__dict__").let { globals -> + Python3.eval(expression, PY_EVAL_INPUT, globals, globals).toString() + } + + private companion object { + /** `Py_eval_input`. */ + const val PY_EVAL_INPUT = 258 + const val INITIAL = "[7, 5000000000, 1.5, True, 'abc']" + const val WRITTEN = "[8, 6000000000, 2.5, False, 'xyz']" + + val ROOTS = """ + _rs_states = {} + _rs_error = [] + + def _rs_root(): + from python.multiplatform.compose import rememberSaveableWrapper + from androidx.compose.foundation.layout import Column + from androidx.compose.material3 import Text + _rs_states['i'] = rememberSaveableWrapper(7) + _rs_states['l'] = rememberSaveableWrapper(5000000000) + _rs_states['f'] = rememberSaveableWrapper(1.5) + _rs_states['b'] = rememberSaveableWrapper(True) + _rs_states['s'] = rememberSaveableWrapper('abc') + def _rs_body(): + for _k in 'ilfbs': + Text(str(_rs_states[_k].value)) + Column(content=_rs_body) + + def _rs_root_bad(): + from python.multiplatform.compose import rememberSaveableWrapper + try: + rememberSaveableWrapper([1, 2]) + except BaseException as _e: + _rs_error.append(type(_e).__name__ + ': ' + str(_e)) + """.trimIndent() + } +} diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/RetainedSlotSweepPreconditionTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/RetainedSlotSweepPreconditionTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/RetainedSlotSweepPreconditionTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/RetainedSlotSweepPreconditionTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/SwipeableRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/SwipeableRenderTest.kt similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/SwipeableRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/SwipeableRenderTest.kt diff --git a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/TextFieldStateRenderTest.kt b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/TextFieldStateRenderTest.kt similarity index 86% rename from ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/TextFieldStateRenderTest.kt rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/TextFieldStateRenderTest.kt index 2fe9a7c3c..dd485d64f 100644 --- a/ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/TextFieldStateRenderTest.kt +++ b/python-multiplatform-compose/fixtures/compose/src/desktopTest/kotlin/fixture/compose/TextFieldStateRenderTest.kt @@ -147,7 +147,36 @@ class TextFieldStateRenderTest { } /** - * Made inside a composition by `rememberTextFieldState`, drawn by `TextField(state=...)` in the same + * Issue #131: the Pythonic spellings are the same declarations as the Kotlin ones -- the + * `@Composable` `remember_text_field_state`, the constructor's `initial_text=`/`initial_selection=` + * keywords, and the extensions `clear_text()`/`set_text_and_place_cursor_at_end()` -- and the + * Kotlin spellings keep working beside them. + */ + @Test + fun thePythonicSpellingsAreTheSameDeclarations() { + Python3.exec( + """ + from androidx.compose.foundation.text.input import ( + TextFieldState, rememberTextFieldState, remember_text_field_state, + ) + from androidx.compose.ui.text import TextRange + + assert remember_text_field_state is rememberTextFieldState + _tfs_py = TextFieldState(initial_text='kw', initial_selection=TextRange(0)) + assert _tfs_py.text == 'kw', repr(_tfs_py.text) + _tfs_py.clear_text() + assert _tfs_py.text == '', repr(_tfs_py.text) + _tfs_py.set_text_and_place_cursor_at_end('snake') + assert _tfs_py.text == 'snake', repr(_tfs_py.text) + _tfs_py.setTextAndPlaceCursorAtEnd('camel') + assert _tfs_py.text == 'camel', repr(_tfs_py.text) + """.trimIndent(), + ) + } + + /** + * Made inside a composition by `remember_text_field_state` (the Pythonic spelling of + * `rememberTextFieldState`, issue #131), drawn by `TextField(state=...)` in the same * body, then written from Python **outside** the composition: the state is Compose snapshot state, * so the next frame of the *same* scene shows the new text with no host call and no Python body * re-run being needed. Every value the body saw is the one remembered state (each reads the written @@ -168,7 +197,7 @@ class TextFieldStateRenderTest { """ assert len(_tfs_seen) >= 1, _tfs_seen assert type(_tfs_seen[0].text) is str and _tfs_seen[0].text == 'abc', repr(_tfs_seen[0].text) - _tfs_seen[0].setTextAndPlaceCursorAtEnd('a much longer line of text') + _tfs_seen[0].set_text_and_place_cursor_at_end('a much longer line of text') """.trimIndent(), ) // `TextField(state=...)` does not finish reacting to a state write inside one frame: the @@ -235,9 +264,9 @@ class TextFieldStateRenderTest { const val MAX_FRAMES = 4 val REMEMBERED = """ - from androidx.compose.foundation.text.input import rememberTextFieldState + from androidx.compose.foundation.text.input import remember_text_field_state from androidx.compose.material3 import TextField - _tfs_state = rememberTextFieldState('abc') + _tfs_state = remember_text_field_state('abc') _tfs_seen.append(_tfs_state) TextField(state=_tfs_state) """.trimIndent() diff --git a/ksp-fixtures/compose/src/desktopTest/resources/opaque-square.png b/python-multiplatform-compose/fixtures/compose/src/desktopTest/resources/opaque-square.png similarity index 100% rename from ksp-fixtures/compose/src/desktopTest/resources/opaque-square.png rename to python-multiplatform-compose/fixtures/compose/src/desktopTest/resources/opaque-square.png diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/README.md b/python-multiplatform-compose/fixtures/notebook-e2e/README.md new file mode 100644 index 000000000..0a7ab678a --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/README.md @@ -0,0 +1,133 @@ +# `:python-multiplatform-compose:fixtures:notebook-e2e`: pythonx-compose's `UI.ipynb`, end to end + +Issue #26. A desktop host draws a Python-declared root through `python-multiplatform-compose` +(`PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")`, #18), with the **pythonx-compose wheel +installed**, and runs the notebook's scenarios against it. Each scenario is its own test, so a +regression fails under the notebook cell's number. + +The specification is the user's notebook, `pythonx-compose/UI.ipynb` (read in place, never copied). +Where pythonx-compose has since decided a different spelling (its `docs/INTENT.md` §5 and +`docs/SPEC.md`), the tests use the decided one: `@app` and an `app_root` state instead of +`main.App.update(...)`. The UI state is the notebook's own: `remember_saveable` inside the root, +attached as `App.messages`, read and written with `getValue()`/`setValue()` (`.value` is an alias), +which needs `python.multiplatform.compose.RememberSaveableKt` in the bindings. Kotlin parameter names in +snake_case; `Color(0xFF...)`; `DefaultIcons.Add` without parentheses; +`TextField(state=..., modifier=Modifier.padding(8))`. + +## Building the wheel + +Build the wheel with uv from a clean copy of pythonx-compose's `develop`, so nothing is written into +that checkout: + +```bash +mkdir -p .tmp/pxc-src +git -C ../pythonx-compose archive origin/develop | tar -x -C .tmp/pxc-src +(cd .tmp/pxc-src && uv build --wheel --out-dir ../pythonx-compose-dist) +``` + +The result is `.tmp/pythonx-compose-dist/pythonx_compose--py3-none-any.whl`. + +## Running + +```bash +JAVA_HOME= ./gradlew :python-multiplatform-compose:fixtures:notebook-e2e:desktopTest --rerun \ + -PpythonxComposeWheel=.tmp/pythonx-compose-dist --console=plain > .tmp/notebook-e2e.log 2>&1; echo "EXIT=$?" +``` + +| Input | Gradle property | Environment | +|---|---|---| +| the wheel: a `.whl` file, or a directory holding `pythonx_compose-*.whl` (relative paths are from the repository root) | `-PpythonxComposeWheel=` | `PYTHONX_COMPOSE_WHEEL` | +| the version to pick from that directory (or to require of the file) | `-PpythonxComposeVersion=` | `PYTHONX_COMPOSE_VERSION` | +| do not run the scenarios | `-PnotebookE2e.skip=true` | `NOTEBOOK_E2E_SKIP=true` | + +**Without a wheel the tests fail, by name, with a message that says how to give one.** They do not +skip. Only `-PnotebookE2e.skip=true` disables `desktopTest`, and the build log then says that the +scenarios did not run. CI wants the scenarios run: the `notebook-e2e` job of `.github/workflows/desktop.yml` (#213) builds +the wheel from pythonx-compose `develop` with uv and runs this module on every push to `develop`. + +"Installing" is unpacking the pure-Python wheel into `build/pythonx-compose/site-packages` +(`installPythonxComposeWheel`), which is all installing a pure-Python wheel amounts to; the embedded +interpreter has no installer of its own. The tests then require `pythonx.compose.__file__` to be +inside that directory, so a stray source checkout on `sys.path` cannot stand in for the wheel. + +## In a GraalVM native image (macOS, #249) + +The same 20 scenarios, built into one executable and run on their own: + +```bash +JAVA_HOME= ./gradlew :python-multiplatform-compose:fixtures:notebook-e2e:notebookE2eNativeRun \ + -PpythonxComposeWheel=.tmp/pythonx-compose-dist --console=plain +``` + +- `NotebookE2eMain` calls each scenario after its class's `@BeforeTest`, with no JUnit, and prints + `NOTEBOOK_E2E: n passed, m failed of 20`. `notebookE2eJvm` runs it on the JVM first. +- `notebookE2eNativeCompile` calls `native-image` from the JDK 25 toolchain (Liberica NIK Full). The + configuration Compose and skiko need is `src/desktopTest/native-image`, recorded by + `notebookE2eNativeAgent`; rerun that task after a dependency changes. +- Beside the executable: `python-multiplatform-home/` (the plugin's `stageNativeImagePythonHome`), + `site-packages/` (the wheel) and `app/`. The image finds them there with no environment. +- `notebookE2eNativeRun` removes `JAVA_HOME` and `PYTHONHOME`, requires 20 passed, and fails if + `otool -L` shows `libjvm`. +- The image takes `ui`, `ui-text` and `foundation` from compose-multiplatform-core-extended's mavenLocal + build (`composeExtended.*` in the root `gradle.properties`): upstream Compose sends its main-thread + work to skiko's Swing queue, which needs AWT, and AWT needs `libjvm`. The fork sends it to + `Dispatchers.Main` under `compose.main.dispatcher=coroutines`, which `NotebookE2eMainDispatcherFactory` + supplies (registered by `src/desktopTest/native-image-main/` on the image's classpath only). + +## Scenarios + +| Notebook cells | Test | What must hold | +|---|---|---| +| 5 | `NotebookRootTest.cell05_theImportsResolveFromTheInstalledWheel` | the cell's imports (decided spellings) resolve | +| 6–7 | `…cell06to07_importingMainDrawsTheAppItDeclares` | `import main` declares the root; the host draws exactly `Text(INITIAL)`; an idle frame does not rerun it | +| 9–13 | `…cell09to13_theNotebookAndTheScreenShareOneState` | the notebook reads the shown state, writes it, the screen follows; restoring restores the screen | +| 15–16 | `…cell15to16_redeclaringTheRootChangesTheScreenWithNoUpdateCall` | a cell redeclaring the root with `@app` puts it on screen within 4 frames, pixel-equal to Kotlin | +| (negative) | `…negative_withAppStubbedOutTheRedeclarationDoesNotReachTheScreen` | with `@app` stubbed to not write `app_root`, the same observation reports no change | +| (negative) | `…negative_aHostHandedTheRootOnceDoesNotFollowARedeclaration` | with the host given `main.App` instead of the state, the same observation reports no change | +| 19, 20 | `NotebookPracticeTest.cell19_text`, `cell20_textWithAFontSize` | red text; at 30 sp | +| 22, 23 | `…cell22_anEmptyBlackButton`, `cell23_aButtonHoldingText` | black `Button`, empty and with text | +| 25 | `…cell25_aCard` | red `Card` with white text | +| 28, 29, 30 | `…cell28_defaultIconsAddIsAnImageVector`, `cell29_anIcon`, `cell30_anIconInsideAButton` | `DefaultIcons.Add` is an `ImageVector`; `Icon` draws it, alone and in a button | +| 35, 36, 38 | `…cell35_aColumnOfTwoButtons`, `cell36_aColumnAlignedToTheEnd`, `cell38_aRow` | `Column`, `Column(horizontal_alignment=Alignment.Horizontal.End)`, `Row` | +| 37 (signature) | `…cell37_aRowWithAnArrangement` | `Row(horizontal_arrangement=Arrangement.Center)` | +| 40 | `…cell40_aCardSpacedBySpacers` | `Card`/`Column`/`Row` spaced by `Spacer(modifier=Modifier.padding(...))` | +| 31–33 | `NotebookTextFieldTest.cell32to33_aTextFieldKeepsInputMethodCompositionInComposeAndTheNotebookReadsTheResult` | see below | + +Every practice cell is compared **pixel for pixel** with the same composables drawn from Kotlin, so a +pass means the screen shows what the cell declared. + +### The text field (cells 31–33, pythonx-compose #10) + +`TextField(state=remember_text_field_state(""))` is focused by a click (keyboard focus traversal if +the click does not do it), then receives a Hangul input-method sequence (composing ㅎ, 하, 한, then +committing 한) with a frame after each event. While it types, a `sys.monitoring` counter of every +Python function started (`PY_START`, all threads) must stay at zero and the root must not rerun. +Kotlin's `TextFieldState` must hold the composing text with composing range `0..1` at each step and +`한` with no composing range after the commit; the notebook then reads `fields[-1].text == "한"`. +Finally `set_text_and_place_cursor_at_end("x")` from outside the composition must reach the screen +within 4 frames. + +**How the input method is driven.** `ImageComposeScene` (Compose Multiplatform 1.11.1) cannot take +input-method composition: it builds its scene with a `PlatformContext.Empty` delegate that never +hands out the text field's `PlatformTextInputMethodRequest`, has no parameter to replace it, and its +public input API is `sendKeyEvent` (key presses) and pointer events. `InputMethodScene` therefore +builds the same `CanvasLayersComposeScene` (`@InternalComposeUiApi`) with its own `PlatformContext` +that records the request, and delivers each event the way the desktop window's +`InputMethodSession.inputMethodTextChanged` does in 1.11.1: one `request.editText { commitText(committed, 1); +if (composing.isNotEmpty()) setComposingText(composing, 1) }`. Not exercised: the AWT layer above it +(decoding an `InputMethodEvent` into those two strings), which needs a window. + +## Gaps: notebook content not expressed here + +| Notebook | Why it is not in a test | +|---|---| +| cells 2–3 (`print`, numpy) | not UI | +| cell 5: `remember_saveable`, `DefaultCoroutineScope`, `MainCoroutineScope` | undecided in pythonx-compose (INTENT §4.1, SPEC §8); nothing provides them | +| cell 5/25/35/…: lower-case `modifier`, `modifier=modifier` | INTENT §5.4 replaces it with `modifier=Modifier`. The binder has no conversion for a proxy **class** passed as an argument (`Modifier.empty()` is used only for the class-spelling *method call*, `PythonxAdapter.kt` `_BoundMember`), so `Card(modifier=Modifier)` is not expected to work; the argument is left out, which is the same Kotlin default (`Modifier`). Not run to confirm. | +| cell 22/23/30/35: `corner_radius=` on `Button`; cell 25/40: on `Card` | Kotlin's counterpart is `shape=RoundedCornerShape(...)` from `androidx.compose.foundation.shape`, which this fixture does not walk (its package list is `:python-multiplatform-compose:fixtures:compose`'s). Adding `"androidx.compose.foundation.shape"` to `artifactIncludePackages` is the next step; not tried. | +| cell 22/23/25/…: `color=` on `Button`/`Card` | written as Kotlin's `colors=ButtonColors(...)` / `CardColors(...)` constructors. `ButtonDefaults.buttonColors(...)`, the usual Kotlin spelling, is a `@Composable` object function, which the walker declines (SPEC B-1). | +| cell 27: `help(DefaultIcons)` | prints documentation; nothing to assert on screen | +| cells 42, 48: empty; "채팅 화면 디자인하기" | no content | +| cells 44–47: `from ws import client` | a websocket client outside pythonx-compose; marked "재 구현 필요" in the notebook | +| cells 51–56: Llama 3.1 streaming into the UI | needs a model and `main.run_llama3`; the state half (writing `.value` from a loop) is cell 12's mechanism | +| cell 54: `onclick=lambda: {...}` driving a state change | a click from Python reaching a Kotlin callback is `:python-multiplatform-compose:fixtures:compose`'s `CallbackDrivenRenderTest`; not repeated here | diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/build.gradle.kts b/python-multiplatform-compose/fixtures/notebook-e2e/build.gradle.kts new file mode 100644 index 000000000..9dc84176f --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/build.gradle.kts @@ -0,0 +1,368 @@ +import java.io.ByteArrayOutputStream +import org.apache.tools.ant.util.TeeOutputStream +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +/** + * Issue #26: pythonx-compose's `UI.ipynb` scenarios, run end to end against a real host. + * + * The host is `python-multiplatform-compose`'s `PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")` + * (#18). The Python side is the **installed pythonx-compose wheel**, not a copy of its sources and + * not a stand-in: every `pythonx.compose.*` import in the tests must resolve to the files the wheel + * unpacked (the tests check `__file__`). The wheel is an input: + * + * | Input | Gradle property | Environment | + * |---|---|---| + * | a `.whl` file, or a directory holding `pythonx_compose-*.whl` | `-PpythonxComposeWheel=` | `PYTHONX_COMPOSE_WHEEL` | + * | which version to pick from that directory (or to require of the file) | `-PpythonxComposeVersion=` | `PYTHONX_COMPOSE_VERSION` | + * | deliberately not running the scenarios | `-PnotebookE2e.skip=true` | `NOTEBOOK_E2E_SKIP=true` | + * + * **No wheel is a failure, not a skip.** Without an input every test fails by name with the message + * below; only the explicit opt-out disables `desktopTest`, and it says so in the build log. CI decides + * which of the two it wants. + * + * "Installing" a pure-Python wheel is unpacking it onto `sys.path`, which is what + * `pip install --target` does for one; the embedded interpreter has no `pip` of its own to run. + * [installPythonxComposeWheel] unpacks it into `build/pythonx-compose/site-packages`. + * + * The rest -- the Compose compiler plugin, the walked packages, the JVM flags -- is + * `:python-multiplatform-compose:fixtures:compose`'s configuration, kept identical where it can be so that a difference in + * result is a difference in what the notebook asks for, not in the build. + */ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.jetpack.compose) + alias(libs.plugins.compose.compiler) + id("io.github.thisisthepy.python.multiplatform.bindings") +} + +kotlin { + jvm("desktop") { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.pythonMultiplatform) + } + } + val desktopMain by getting { + dependencies { + implementation(projects.pythonMultiplatformCompose) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(libs.compose.material.icons.core) + implementation(compose.desktop.currentOs) + } + } + val desktopTest by getting { + dependencies { + implementation(libs.kotlin.test) + } + } + } +} + +pythonBindings { + role.set("app") + processor.set(projects.pythonMultiplatformKsp) + + artifactConfiguration.set("desktopCompileClasspath") + artifactSourceSet.set("desktopMain") + // The same list as `:python-multiplatform-compose:fixtures:compose` (its build file says what each entry is for). Every + // `pythonx.compose.*` module the notebook imports maps onto one of these Kotlin packages + // (pythonx-compose's `pythonx-map.toml`); a package not walked here is a name pythonx cannot + // resolve, which is a fixture gap, not a pythonx one. + artifactIncludePackages.set( + listOf( + "androidx.compose.material3", + "androidx.compose.foundation.layout", + "androidx.compose.ui.graphics", + "androidx.compose.ui.res", + "androidx.compose.ui", + "androidx.compose.material.icons", + "androidx.compose.runtime.SnapshotStateKt", + "androidx.compose.runtime.State", + "androidx.compose.runtime.MutableState", + "androidx.compose.foundation.text.input.TextFieldState", + "androidx.compose.foundation.text.input.TextFieldStateKt", + "python.multiplatform.compose.RememberSaveableKt", + ), + ) +} + +// ---- The pythonx-compose wheel --------------------------------------------------------------------- + +val wheelInput: String? = providers.gradleProperty("pythonxComposeWheel") + .orElse(providers.environmentVariable("PYTHONX_COMPOSE_WHEEL")).orNull?.takeIf { it.isNotBlank() } +val wheelVersion: String? = providers.gradleProperty("pythonxComposeVersion") + .orElse(providers.environmentVariable("PYTHONX_COMPOSE_VERSION")).orNull?.takeIf { it.isNotBlank() } +val notebookSkipped: Boolean = providers.gradleProperty("notebookE2e.skip") + .orElse(providers.environmentVariable("NOTEBOOK_E2E_SKIP")).orNull?.toBoolean() == true + +/** The wheel to install, or why there is none. Exactly one of the two is non-null. */ +fun resolveWheel(): Pair { + val howTo = "Build it (python-multiplatform-compose/fixtures/notebook-e2e/README.md) and pass -PpythonxComposeWheel= " + + "(or PYTHONX_COMPOSE_WHEEL); to skip the notebook scenarios deliberately pass -PnotebookE2e.skip=true." + val input = wheelInput + ?: return null to "No pythonx-compose wheel was given, so the UI.ipynb scenarios cannot run. $howTo" + val given = rootProject.file(input) + if (!given.exists()) return null to "The pythonx-compose wheel input does not exist: $given. $howTo" + val versionPart = wheelVersion?.let { "pythonx_compose-$it-" } + fun isWheel(f: File) = f.isFile && f.name.startsWith("pythonx_compose-") && f.name.endsWith(".whl") + if (given.isDirectory) { + val candidates = given.listFiles().orEmpty().filter(::isWheel) + .filter { versionPart == null || it.name.startsWith(versionPart) } + .sortedBy { it.name } + return when (candidates.size) { + 1 -> candidates.single() to null + 0 -> null to "No pythonx_compose-${wheelVersion ?: "*"}-*.whl in $given. $howTo" + else -> null to "Several pythonx-compose wheels in $given (${candidates.joinToString { it.name }}); " + + "choose one with -PpythonxComposeVersion= or pass the file itself." + } + } + if (!isWheel(given)) return null to "$given is not a pythonx_compose-*.whl file. $howTo" + if (versionPart != null && !given.name.startsWith(versionPart)) { + return null to "$given is not version $wheelVersion (-PpythonxComposeVersion)." + } + return given to null +} + +val resolvedWheel = resolveWheel() +val pythonxComposeWheel: File? = resolvedWheel.first +val pythonxComposeWheelError: String? = resolvedWheel.second +val pythonxComposeSitePackages = layout.buildDirectory.dir("pythonx-compose/site-packages") + +val installPythonxComposeWheel by tasks.registering(Sync::class) { + group = "verification" + description = "Unpacks the pythonx-compose wheel (-PpythonxComposeWheel) where the notebook E2E tests put it on sys.path." + val wheel = pythonxComposeWheel + enabled = wheel != null + if (wheel != null) from(zipTree(wheel)) + into(pythonxComposeSitePackages) +} + +if (notebookSkipped) { + logger.lifecycle( + "notebook-e2e: desktopTest is DISABLED by -PnotebookE2e.skip=true -- the UI.ipynb scenarios did not run.", + ) +} + +/** + * The same JVM `:python-multiplatform-compose:fixtures:compose`'s tests need: JDK 21 with the preview FFM API, a + * `java.library.path` for the extracted `libpython`, and a headless AWT. + */ +tasks.named("desktopTest") { + javaLauncher.set( + javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(21)) + }, + ) + jvmArgs("--enable-preview", "-Djava.library.path=.") + systemProperty("java.awt.headless", "true") + + // The application module the notebook imports as `main`. + val appDir = layout.projectDirectory.dir("src/desktopTest/python") + inputs.dir(appDir) + systemProperty("notebookE2e.appDir", appDir.asFile.absolutePath) + + val wheel = pythonxComposeWheel + if (wheel != null) { + dependsOn(installPythonxComposeWheel) + inputs.file(wheel) + systemProperty("notebookE2e.wheel", wheel.absolutePath) + systemProperty("notebookE2e.sitePackages", pythonxComposeSitePackages.get().asFile.absolutePath) + } else { + systemProperty("notebookE2e.wheelError", checkNotNull(pythonxComposeWheelError)) + } + enabled = !notebookSkipped +} + +// ---- The notebook E2E in a GraalVM native image (issue #249, #191's first milestone) ---------- +// +// `NotebookE2eMain` runs the same 20 scenarios as `desktopTest`, by direct calls, so they can run in a +// native image. The scenes are offscreen (`ImageComposeScene`), so no window is involved. Built the way +// `:sample:nativeCompile` builds its image: `native-image` called directly, because the GraalVM Gradle +// plugin registers no tasks for a Kotlin Multiplatform `jvm()` target. The Compose and Skiko +// configuration a closed world needs is recorded by `notebookE2eNativeAgent` into +// `src/desktopTest/native-image` and committed; python-multiplatform's own FFM registrations come +// from its jar's `META-INF/native-image`. + +val e2eTestCompilation = kotlin.targets.getByName("desktop").compilations.getByName("test") +// `native-image-main/` registers the AWT-free `Dispatchers.Main` (`NotebookE2eMainDispatcher.kt`), first on +// the path so it is found before kotlinx-coroutines-swing's; only the agent run and the image use it. +val e2eNativeMainResources = layout.projectDirectory.dir("src/desktopTest/native-image-main") +// The fork's Compose (#191): its `ui-desktop` sends the scene's main-thread work to `Dispatchers.Main` +// when it is built with `-Dcompose.awt=false` (`ExtendedMainDispatcher`), where upstream's always uses +// skiko's `SwingDispatcher`, which starts AWT; and AWT in a native image needs the JDK's `libawt`, +// which links `libjvm`. So the image takes the modules `composeExtended.modules` names from the fork, +// at `composeExtended.version` (root gradle.properties); `desktopTest` keeps upstream Compose. +val composeExtendedVersion = providers.gradleProperty("composeExtended.version").orNull +// The fork's group prefix: `org.jetbrains.compose` (its old coordinates) or its own, e.g. +// `org.thisisthepy.compose`; a module `org.jetbrains.compose.ui:ui-desktop` is then taken as +// `.ui:ui-desktop`. +val composeExtendedGroupPrefix = providers.gradleProperty("composeExtended.groupPrefix").orNull ?: "org.jetbrains.compose" +val composeExtendedModules = providers.gradleProperty("composeExtended.modules").orNull + ?.split(',')?.map { it.trim() }?.filter { it.isNotEmpty() }?.toSet().orEmpty() +// Where the fork's modules come from: this machine's mavenLocal (`composeExtended.mavenLocal`), or a +// Maven repository by URL (`composeExtended.repository`, e.g. the fork's GitHub Packages registry, +// #251), read with `GITHUB_ACTOR`/`GITHUB_TOKEN` when they are set (GitHub Packages asks for a token +// even for a public package). Either way only the fork's group at the fork's version is taken there. +val composeExtendedMavenLocal = providers.gradleProperty("composeExtended.mavenLocal").orNull?.toBoolean() == true +val composeExtendedRepository = providers.gradleProperty("composeExtended.repository").orNull?.takeIf { it.isNotBlank() } +if (composeExtendedVersion != null && (composeExtendedMavenLocal || composeExtendedRepository != null)) { + val forkOnly: MavenRepositoryContentDescriptor.() -> Unit = { + includeVersionByRegex(Regex.escape(composeExtendedGroupPrefix) + ".*", ".*", Regex.escape(composeExtendedVersion)) + } + // A project's own repositories replace the settings' ones, so those are repeated here. + repositories { + if (composeExtendedMavenLocal) mavenLocal { mavenContent(forkOnly) } + if (composeExtendedRepository != null) { + maven { + url = uri(composeExtendedRepository) + System.getenv("GITHUB_TOKEN")?.let { token -> + credentials { + username = System.getenv("GITHUB_ACTOR") ?: "token" + password = token + } + } + mavenContent(forkOnly) + } + } + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} +val e2eNativeRuntime: Configuration = configurations.getByName("desktopTestRuntimeClasspath").copyRecursive().apply { + resolutionStrategy.eachDependency { + if (composeExtendedVersion != null && "${requested.group}:${requested.name}" in composeExtendedModules) { + useTarget("${requested.group.replaceFirst("org.jetbrains.compose", composeExtendedGroupPrefix)}:${requested.name}:$composeExtendedVersion") + because("compose-multiplatform-core-extended: an AWT-free main dispatcher for a native image (#249)") + } + } +} +// The copied configuration carries the dependencies only; the main compilation's output and the +// walker's composable thunks (generated bytecode, not a dependency) are added as they are for desktopTest. +val e2eMainCompilation = kotlin.targets.getByName("desktop").compilations.getByName("main") +val e2eClasspath = files( + e2eNativeMainResources, + e2eTestCompilation.output.allOutputs, + e2eMainCompilation.output.allOutputs, + layout.buildDirectory.dir("generated/pythonComposableThunks/desktopMain"), + e2eNativeRuntime, +) +val e2eMainClass = "fixture.notebook.NotebookE2eMainKt" +val e2eNativeConfig = layout.projectDirectory.dir("src/desktopTest/native-image") +val e2eNativeDir = layout.buildDirectory.dir("native/notebook-e2e") +val e2eAppDir = layout.projectDirectory.dir("src/desktopTest/python") +val e2eJdk25 = javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(25)) } + +fun JavaExec.notebookE2eOnTheJvm() { + dependsOn(e2eTestCompilation.compileTaskProvider, installPythonxComposeWheel) + classpath = e2eClasspath + mainClass.set(e2eMainClass) + javaLauncher.set(e2eJdk25) + jvmArgs("--enable-native-access=ALL-UNNAMED", "-Djava.awt.headless=true", "-Dcompose.awt=false") + systemProperty("notebookE2e.appDir", e2eAppDir.asFile.absolutePath) + systemProperty("notebookE2e.sitePackages", pythonxComposeSitePackages.get().asFile.absolutePath) + pythonxComposeWheel?.let { systemProperty("notebookE2e.wheel", it.absolutePath) } + pythonxComposeWheelError?.let { systemProperty("notebookE2e.wheelError", it) } +} + +tasks.register("notebookE2eJvm") { + group = "verification" + description = "Runs NotebookE2eMain on the JVM: the 20 scenarios outside JUnit, before a native image is built from it." + notebookE2eOnTheJvm() +} + +tasks.register("notebookE2eNativeAgent") { + group = "native" + description = "Runs NotebookE2eMain under native-image's tracing agent and writes the configuration it records into src/desktopTest/native-image." + notebookE2eOnTheJvm() + doFirst { e2eNativeConfig.asFile.mkdirs() } + jvmArgs("-agentlib:native-image-agent=config-output-dir=${e2eNativeConfig.asFile.absolutePath}") +} + +pythonBindings { + nativeImageDirectory.set(e2eNativeDir) +} + +val notebookE2eNativeStage by tasks.registering(Sync::class) { + group = "native" + description = "Puts the pythonx-compose wheel and the notebook's app beside the native image." + dependsOn(installPythonxComposeWheel) + from(pythonxComposeSitePackages) { into("site-packages") } + from(e2eAppDir) { into("app") } + into(layout.buildDirectory.dir("native/notebook-e2e-files")) +} + +val notebookE2eNativeCompile by tasks.registering(Exec::class) { + group = "native" + description = "Builds the notebook E2E as a GraalVM native image (build/native/notebook-e2e/notebook-e2e)." + dependsOn(e2eTestCompilation.compileTaskProvider, "stageNativeImagePythonHome", notebookE2eNativeStage) + inputs.files(e2eClasspath) + inputs.dir(e2eNativeConfig) + outputs.file(e2eNativeDir.map { it.file("notebook-e2e") }) + val launcher = e2eJdk25 + val outDir = e2eNativeDir + val staged = layout.buildDirectory.dir("native/notebook-e2e-files") + doFirst { outDir.get().asFile.mkdirs() } + executable = launcher.get().metadata.installationPath.asFile.resolve("bin/native-image").absolutePath + args( + "-cp", e2eClasspath.asPath, + "--no-fallback", + "--enable-native-access=ALL-UNNAMED", + "-Djava.awt.headless=true", + // The fork's switch: `ExtendedAwt.available` is a constant false, so nothing reaches AWT. + "-Dcompose.awt=false", + "-H:ConfigurationFileDirectories=${e2eNativeConfig.asFile.absolutePath}", + "-o", outDir.get().asFile.resolve("notebook-e2e").absolutePath, + e2eMainClass, + ) + doLast { + project.copy { from(staged); into(outDir) } + } +} + +tasks.register("notebookE2eNativeRun") { + group = "verification" + description = "Runs the native image on its own (no JAVA_HOME, no PYTHONHOME) and requires 20 of 20 scenarios to pass and no libjvm." + dependsOn(notebookE2eNativeCompile) + val dir = e2eNativeDir + workingDir(dir) + executable = dir.get().asFile.resolve("notebook-e2e").absolutePath + environment.remove("JAVA_HOME") + environment.remove("PYTHONHOME") + val captured = ByteArrayOutputStream() + standardOutput = TeeOutputStream(System.out, captured) + isIgnoreExitValue = true + doLast { + val out = captured.toString() + val summary = out.lineSequence().lastOrNull { it.startsWith("NOTEBOOK_E2E:") } + ?: throw GradleException("the native image printed no NOTEBOOK_E2E summary") + if (summary != "NOTEBOOK_E2E: 20 passed, 0 failed of 20") throw GradleException("notebook E2E in the native image: $summary") + val otool = ProcessBuilder("otool", "-L", executable).redirectErrorStream(true).start() + val linked = otool.inputStream.bufferedReader().readText().also { otool.waitFor() } + if ("libjvm" in linked) throw GradleException("the native image links libjvm:\n$linked") + logger.lifecycle("notebook E2E native image: $summary; no libjvm") + } +} + +tasks.register("notebookE2eNativeClasspath") { + group = "native" + description = "Lists the Compose artefacts the notebook E2E's native image is built from." + val files = e2eNativeRuntime + doLast { files.files.map { it.name }.filter { it.startsWith("ui") || it.startsWith("foundation") || it.startsWith("skiko") }.sorted().forEach { println(it) } } +} diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopMain/kotlin/fixture/notebook/EmptyModifier.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopMain/kotlin/fixture/notebook/EmptyModifier.kt new file mode 100644 index 000000000..a1c4509fc --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopMain/kotlin/fixture/notebook/EmptyModifier.kt @@ -0,0 +1,14 @@ +package fixture.notebook + +import androidx.compose.ui.Modifier + +/** + * The one line of Kotlin pythonx-compose asks an application for: where an empty `Modifier` comes from. + * + * `Modifier` as a Kotlin expression is `Modifier.Companion`, an object, and the walker binds functions, + * so the empty modifier every `Modifier.padding(8)` chain starts from has no bound name of its own. + * pythonx-compose's `pythonx/compose/ui/modifier.py` says what to do about it: supply a zero-argument + * function returning `Modifier` and register its name with `install(...)`. KSP binds this one as + * `fixture.notebook.emptyModifier`; `NotebookHost` registers it. + */ +fun emptyModifier(): Modifier = Modifier diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/InputMethodScene.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/InputMethodScene.kt new file mode 100644 index 000000000..5f85afb3c --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/InputMethodScene.kt @@ -0,0 +1,127 @@ +@file:OptIn(InternalComposeUiApi::class, ExperimentalComposeUiApi::class) + +package fixture.notebook + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.platform.PlatformContext +import androidx.compose.ui.platform.PlatformTextInputMethodRequest +import androidx.compose.ui.platform.WindowInfo +import androidx.compose.ui.scene.CanvasLayersComposeScene +import androidx.compose.ui.scene.ComposeScene +import androidx.compose.ui.text.input.TextEditingScope +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitCancellation +import org.jetbrains.skia.Image +import org.jetbrains.skia.Surface + +/** + * An offscreen scene that, unlike `ImageComposeScene`, lets a test act as the **input method**. + * + * ### Why not `ImageComposeScene` + * + * In Compose Multiplatform 1.11.1 `ImageComposeScene` builds its `ComposeScene` with + * `CanvasLayersComposeScene(..., platformContext = )` (read from + * `ImageComposeScene.class` and `ImageComposeScene$_platformContext$1.class` in `ui-desktop-1.11.1.jar`). + * `PlatformContext.Empty` never hands out the `PlatformTextInputMethodRequest` a focused text field + * starts an input session with, and `ImageComposeScene` has no parameter to replace the context. Its + * public API offers `sendKeyEvent` only -- key presses, not input-method composition. + * + * ### What this does instead + * + * The same `CanvasLayersComposeScene` `ImageComposeScene` wraps (`@InternalComposeUiApi`), drawn into a + * Skia raster surface exactly as `ImageComposeScene.render` does, with a [PlatformContext] whose + * `startInputMethod` records the request a focused field starts. [inputMethodEvent] then delivers an + * input-method event the way the desktop window does: `InputMethodSession.inputMethodTextChanged` in + * the same jar turns an AWT `InputMethodEvent` into one `request.editText { ... }` call that does + * `commitText(committed, 1)` and then, if there is composing text, `setComposingText(composing, 1)`. + * That is reproduced here call for call. What is *not* exercised is the AWT layer above it (decoding an + * `InputMethodEvent`'s attributed text into those two strings), which needs a window. + */ +internal class InputMethodScene( + private val width: Int, + private val height: Int, + content: @Composable () -> Unit, +) : AutoCloseable { + + private val requests = ArrayList() + + /** The input session a focused text field started, if one is open. */ + val activeRequest: PlatformTextInputMethodRequest? get() = requests.lastOrNull() + + private val focusedWindow = object : WindowInfo { + override val isWindowFocused: Boolean get() = true + override val containerSize: IntSize get() = IntSize(width, height) + } + + private val context = object : PlatformContext by PlatformContext.Empty() { + override val windowInfo: WindowInfo get() = focusedWindow + + override suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing { + requests += request + try { + awaitCancellation() + } finally { + requests -= request + } + } + } + + val scene: ComposeScene = CanvasLayersComposeScene( + Density(1f), + LayoutDirection.Ltr, + IntSize(width, height), + Dispatchers.Unconfined, + context, + ) + + private val surface = Surface.makeRasterN32Premul(width, height) + + init { + scene.setContent(content) + } + + /** `ImageComposeScene.render()`'s body: clear, draw the scene at time zero, snapshot. */ + fun render(): Image { + surface.canvas.clear(0) + scene.render(surface.canvas.asComposeCanvas(), 0L) + return surface.makeImageSnapshot() + } + + /** A primary-button click at [at], as `ImageComposeScene.sendPointerEvent` delivers one. */ + fun click(at: Offset) { + scene.sendPointerEvent(PointerEventType.Move, at) + scene.sendPointerEvent(PointerEventType.Press, at) + scene.sendPointerEvent(PointerEventType.Release, at) + } + + /** Keyboard-style focus traversal into the first focusable, the fallback when a click did not focus. */ + fun focusFirst(): Boolean = scene.focusManager.takeFocus(FocusDirection.Next) + + /** + * One input-method event: [committed] text committed and [composing] text left in composition -- + * the two strings the desktop window reads from an AWT `InputMethodEvent`. + */ + fun inputMethodEvent(committed: String, composing: String) { + val request = checkNotNull(activeRequest) { "no text input session is open: the field is not focused" } + val edit = checkNotNull(request.editText) { "the input session offers no editText" } + val event: TextEditingScope.() -> Unit = { + commitText(committed, 1) + if (composing.isNotEmpty()) setComposingText(composing, 1) + } + edit(event) + } + + override fun close() { + scene.close() + surface.close() + } +} diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookE2eMain.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookE2eMain.kt new file mode 100644 index 000000000..27693f071 --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookE2eMain.kt @@ -0,0 +1,74 @@ +package fixture.notebook + +import java.io.File +import kotlin.system.exitProcess + +/** + * The notebook E2E as one executable (issue #249, #191's first milestone): the same 20 scenarios the + * `desktopTest` suite runs, called directly, so they can run inside a GraalVM native image, where + * there is no JUnit and no reflection to find a test with. + * + * Each scenario is its class's `@BeforeTest` (`host()`) and then the test method, exactly what JUnit + * does for these classes (none has an `@AfterTest`). A scenario passes when it returns and fails when + * it throws; the summary line is what `notebookE2eNativeRun` checks, and the exit status is non-zero + * on any failure. + * + * The wheel's `site-packages` and the application directory are the system properties the suite + * reads (`NotebookHost`); when they are not set -- a native image started by hand -- they are + * `site-packages/` and `app/` beside the executable, where the build stages them. + */ +fun main() { + // The fork's switch (compose-multiplatform-core-extended `ExtendedMainDispatcher`): Compose's + // main-thread work goes to `Dispatchers.Main`, which `NotebookE2eMainDispatcherFactory` supplies + // with no AWT, instead of skiko's Swing event queue. Read once, before Compose starts; upstream + // Compose ignores it. + System.setProperty("compose.main.dispatcher", "coroutines") + besideTheExecutable("notebookE2e.sitePackages", "site-packages") + besideTheExecutable("notebookE2e.appDir", "app") + + val scenarios: List Unit>> = listOf( + "cell05_theImportsResolveFromTheInstalledWheel" to { NotebookRootTest().run { host(); cell05_theImportsResolveFromTheInstalledWheel() } }, + "cell06to07_importingMainDrawsTheAppItDeclares" to { NotebookRootTest().run { host(); cell06to07_importingMainDrawsTheAppItDeclares() } }, + "cell09to13_theNotebookAndTheScreenShareOneState" to { NotebookRootTest().run { host(); cell09to13_theNotebookAndTheScreenShareOneState() } }, + "cell15to16_redeclaringTheRootChangesTheScreenWithNoUpdateCall" to { NotebookRootTest().run { host(); cell15to16_redeclaringTheRootChangesTheScreenWithNoUpdateCall() } }, + "negative_withAppStubbedOutTheRedeclarationDoesNotReachTheScreen" to { NotebookRootTest().run { host(); negative_withAppStubbedOutTheRedeclarationDoesNotReachTheScreen() } }, + "negative_aHostHandedTheRootOnceDoesNotFollowARedeclaration" to { NotebookRootTest().run { host(); negative_aHostHandedTheRootOnceDoesNotFollowARedeclaration() } }, + "cell19_text" to { NotebookPracticeTest().run { host(); cell19_text() } }, + "cell20_textWithAFontSize" to { NotebookPracticeTest().run { host(); cell20_textWithAFontSize() } }, + "cell22_anEmptyBlackButton" to { NotebookPracticeTest().run { host(); cell22_anEmptyBlackButton() } }, + "cell23_aButtonHoldingText" to { NotebookPracticeTest().run { host(); cell23_aButtonHoldingText() } }, + "cell25_aCard" to { NotebookPracticeTest().run { host(); cell25_aCard() } }, + "cell28_defaultIconsAddIsAnImageVector" to { NotebookPracticeTest().run { host(); cell28_defaultIconsAddIsAnImageVector() } }, + "cell29_anIcon" to { NotebookPracticeTest().run { host(); cell29_anIcon() } }, + "cell30_anIconInsideAButton" to { NotebookPracticeTest().run { host(); cell30_anIconInsideAButton() } }, + "cell35_aColumnOfTwoButtons" to { NotebookPracticeTest().run { host(); cell35_aColumnOfTwoButtons() } }, + "cell36_aColumnAlignedToTheEnd" to { NotebookPracticeTest().run { host(); cell36_aColumnAlignedToTheEnd() } }, + "cell37_aRowWithAnArrangement" to { NotebookPracticeTest().run { host(); cell37_aRowWithAnArrangement() } }, + "cell38_aRow" to { NotebookPracticeTest().run { host(); cell38_aRow() } }, + "cell40_aCardSpacedBySpacers" to { NotebookPracticeTest().run { host(); cell40_aCardSpacedBySpacers() } }, + "cell32to33_aTextFieldKeepsInputMethodCompositionInComposeAndTheNotebookReadsTheResult" to { + NotebookTextFieldTest().run { host(); cell32to33_aTextFieldKeepsInputMethodCompositionInComposeAndTheNotebookReadsTheResult() } + }, + ) + + var failed = 0 + for ((name, scenario) in scenarios) { + try { + scenario() + println("PASS $name") + } catch (t: Throwable) { + failed++ + println("FAIL $name: $t") + t.printStackTrace(System.out) + } + } + println("NOTEBOOK_E2E: ${scenarios.size - failed} passed, $failed failed of ${scenarios.size}") + System.out.flush() + exitProcess(if (failed == 0) 0 else 1) +} + +private fun besideTheExecutable(property: String, directory: String) { + if (System.getProperty(property) != null) return + val executable = ProcessHandle.current().info().command().orElse(null) ?: return + System.setProperty(property, File(File(executable).absoluteFile.parentFile, directory).path) +} diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookE2eMainDispatcher.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookE2eMainDispatcher.kt new file mode 100644 index 000000000..a30a81935 --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookE2eMainDispatcher.kt @@ -0,0 +1,45 @@ +package fixture.notebook + +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.internal.MainDispatcherFactory +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext + +/** + * `Dispatchers.Main` for the notebook E2E's native image (issue #249): one dedicated thread, with no + * AWT behind it. + * + * Compose's main-thread work (`GlobalSnapshotManager`, `postDelayed`) goes to skiko's `SwingDispatcher` + * on upstream Compose, which starts AWT. The fork's Compose sends it to `Dispatchers.Main` instead when + * `compose.main.dispatcher` is `coroutines` (`NotebookE2eMain` sets it), and this supplies that. In a native image AWT + * needs the JDK's `libawt*.dylib` beside the executable, and those link `libjvm`, which the + * milestone rules out. The scenes are offscreen, so nothing needs the event dispatch thread; what + * Compose needs is a single thread its snapshot work is serialised on, which is what the EDT is in + * the headless JVM `desktopTest` runs in. + * + * Registered only on the native image's classpath (`src/desktopTest/native-image-main/`), where its + * [loadPriority] wins over Swing's; `desktopTest` keeps Swing, as before. + */ +@OptIn(InternalCoroutinesApi::class) +class NotebookE2eMainDispatcherFactory : MainDispatcherFactory { + + override val loadPriority: Int = Int.MAX_VALUE / 2 + + override fun createDispatcher(allFactories: List): MainCoroutineDispatcher = NotebookE2eMain + + override fun hintOnError(): String = "the notebook E2E's single-thread Main dispatcher" +} + +private object NotebookE2eMain : MainCoroutineDispatcher() { + + private val thread = Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, "notebook-e2e-main").apply { isDaemon = true } + } + + override val immediate: MainCoroutineDispatcher get() = this + + override fun dispatch(context: CoroutineContext, block: Runnable) = thread.execute(block) + + override fun toString(): String = "NotebookE2eMain" +} diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookHost.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookHost.kt new file mode 100644 index 000000000..68c7609cc --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookHost.kt @@ -0,0 +1,229 @@ +package fixture.notebook + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.unit.Density +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.Image +import python.multiplatform.compose.PythonAppView +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.pythonx.PythonxAdapter +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.ffi.upcall.UpcallBootstrap +import python.multiplatform.generated.FunctionTable +import python.multiplatform.generated.artifacts.ArtifactTable +import python.multiplatform.reflection.UpcallTable +import kotlin.test.fail + +/** + * The host the notebook talks to: one interpreter, the pythonx-compose **wheel** on `sys.path`, and a + * scene drawing `PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")` -- the line pythonx-compose's + * SPEC S5.4 says a host is configured with. + * + * The interpreter and the upcall table are installed once per test JVM and never cleared: a + * `pythonx.compose.*` module keeps the names it resolved in its own dict, and reinstalling the table + * would invalidate the proxies behind them. What a notebook session would carry between cells is reset + * per test instead ([install] calls [reset]): a fresh `app_root` state and a fresh `import main`. + * + * Python code runs in `__main__`, as a notebook kernel's cells do. + */ +internal object NotebookHost { + + /** `Py_eval_input`. */ + private const val PY_EVAL_INPUT = 258 + + /** The upper bound on frames a change may take to reach the screen (TextFieldStateRenderTest's). */ + const val MAX_FRAMES = 4 + + private var installed = false + private var installFailure: Throwable? = null + + /** Called from every test's `@BeforeTest`. Fails -- never skips -- when the wheel is not there. */ + fun install() { + installFailure?.let { throw AssertionError("the notebook host could not be installed: ${it.message}", it) } + if (!installed) { + try { + installOnce() + } catch (t: Throwable) { + installFailure = t + throw t + } + installed = true + } + reset() + } + + private fun installOnce() { + System.getProperty("notebookE2e.wheelError")?.let { fail("notebook-e2e: $it") } + val sitePackages = System.getProperty("notebookE2e.sitePackages") + ?: fail("notebook-e2e: no notebookE2e.sitePackages system property; run this through Gradle's desktopTest") + val wheel = System.getProperty("notebookE2e.wheel") ?: "(unknown wheel)" + val appDir = System.getProperty("notebookE2e.appDir") + ?: fail("notebook-e2e: no notebookE2e.appDir system property; run this through Gradle's desktopTest") + if (!java.io.File(sitePackages, "pythonx/compose/runtime/__init__.py").isFile) { + fail("notebook-e2e: $wheel was not unpacked into $sitePackages (no pythonx/compose/runtime/__init__.py there)") + } + + Python3.initialize(silent = true) + UpcallTable.clear() + UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments) + check(UpcallBootstrap.publishToGlobals()) { "UpcallBootstrap.publishToGlobals() failed" } + PythonProxySource.install() + PythonxAdapter.install(setOf("androidx.compose.ui.unit.Dp")) + + Python3.exec( + """ + import sys, os, functools + sys.path[0:0] = [${pyLiteral(sitePackages)}, ${pyLiteral(appDir)}] + + import pythonx.compose + import pythonx.compose.runtime as _nb_rt + _nb_site = os.path.realpath(${pyLiteral(sitePackages)}) + os.sep + for _nb_m in (pythonx.compose, _nb_rt): + assert os.path.realpath(_nb_m.__file__).startswith(_nb_site), \ + _nb_m.__name__ + ' was imported from ' + _nb_m.__file__ + ', not from the installed wheel ' + ${pyLiteral(wheel)} + _nb_real_app = _nb_rt.app + + # `Modifier.padding(8)` starts from an empty Modifier; pythonx-compose asks the application + # for the Kotlin function that makes one (pythonx/compose/ui/modifier.py). + import pythonx.compose.ui.modifier as _nb_modifier + _nb_modifier.install('fixture.notebook.emptyModifier') + + # A counter of every Python function that starts while it is on (sys.monitoring, every + # thread). The callback and both switches are C callables, so turning it on and off from + # Kotlin runs no Python bytecode of its own and records nothing. + _nb_tool = next(_i for _i in range(6) if sys.monitoring.get_tool(_i) is None) + sys.monitoring.use_tool_id(_nb_tool, 'notebook-e2e') + _nb_started = {} + sys.monitoring.register_callback(_nb_tool, sys.monitoring.events.PY_START, _nb_started.__setitem__) + _nb_monitor_on = functools.partial(sys.monitoring.set_events, _nb_tool, sys.monitoring.events.PY_START) + _nb_monitor_off = functools.partial(sys.monitoring.set_events, _nb_tool, 0) + """.trimIndent(), + ) + } + + /** + * What a fresh notebook session sees after cell 6 (`import main`): a new root state, the real `@app` + * (a negative test may have stubbed it), and `main` imported again so its state starts over. + */ + fun reset() { + Python3.exec( + """ + _nb_monitor_off() + _nb_rt.app = _nb_real_app + _nb_rt.__dict__.pop('app_root', None) + sys.modules.pop('main', None) + import main + """.trimIndent(), + ) + } + + /** Runs a notebook cell in `__main__`. */ + fun cell(source: String) = Python3.exec(source.trimIndent()) + + fun pyStr(expression: String): String = Python3.import("__main__").getAttr("__dict__").let { globals -> + Python3.eval(expression, PY_EVAL_INPUT, globals, globals).toString() + } + + fun pyInt(expression: String): Int = pyStr(expression).toInt() + + /** The host: `PythonAppView` reading pythonx-compose's root state. */ + fun hostScene(width: Int, height: Int): ImageComposeScene = + ImageComposeScene(width = width, height = height, density = Density(1f)) { + PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root") + } + + /** What [content] draws from Kotlin, the control a Python-drawn screen is compared with. */ + fun controlPixels(width: Int, height: Int, content: @Composable () -> Unit): IntArray { + val scene = ImageComposeScene(width = width, height = height, density = Density(1f), content = content) + try { + return pixelsOf(scene.render()) + } finally { + scene.close() + } + } + + /** + * What a window's frame clock does on its own: deliver the snapshot's pending writes, then draw. + * Nothing else -- no host call -- happens between a Python write and the frame that shows it. + */ + fun nextFrame(render: () -> Image): IntArray { + Snapshot.sendApplyNotifications() + return pixelsOf(render()) + } + + /** Up to [MAX_FRAMES] frames, stopping at the first one [done] accepts. Returns it and the count. */ + fun framesUntil(render: () -> Image, done: (IntArray) -> Boolean): Pair { + var frames = 0 + var last: IntArray + do { + last = nextFrame(render) + frames++ + } while (frames < MAX_FRAMES && !done(last)) + return last to frames + } + + /** One redeclaration observed: the screen before the cell, after it, and what Kotlin draws for it. */ + class Redeclaration(val before: IntArray, val after: IntArray, val frames: Int, val expected: IntArray) { + val matchesExpected: Boolean get() = after.contentEquals(expected) + val changedPixels: Int get() = differing(before, after) + val pixelsOffExpected: Int get() = differing(after, expected) + + fun describe(): String = + "after $frames frame(s) $changedPixels pixels changed and $pixelsOffExpected differ from the Kotlin control " + + "(ink before ${inkOf(before)}, after ${inkOf(after)}, control ${inkOf(expected)})" + } + + /** + * Runs [cellSource] -- a cell that redeclares the root -- against a live [scene], then lets up to + * [MAX_FRAMES] frames pass. The positive tests assert [Redeclaration.matchesExpected]; the + * negative ones disable the wiring and assert that this same observation reports no change. + */ + fun redeclare(scene: ImageComposeScene, cellSource: String, expected: IntArray): Redeclaration { + val before = pixelsOf(scene.render()) + cell(cellSource) + val (after, frames) = framesUntil({ scene.render() }) { it.contentEquals(expected) } + return Redeclaration(before, after, frames, expected) + } + + /** + * Runs [block] with the Python-function counter on, and returns what started: one line per code + * object (`qualname file:line`). Empty means no Python function ran. + */ + fun pythonFunctionsStartedDuring(block: () -> Unit): List { + cell("_nb_started.clear()") + val main = Python3.import("__main__") + val on = main.getAttr("_nb_monitor_on") + val off = main.getAttr("_nb_monitor_off") + on().close() + try { + block() + } finally { + off().close() + on.close() + off.close() + } + val count = pyInt("len(_nb_started)") + if (count == 0) return emptyList() + return pyStr( + "'\\n'.join(sorted('%s %s:%d' % (_c.co_qualname, _c.co_filename, _c.co_firstlineno) for _c in _nb_started))", + ).lines() + } + + fun pixelsOf(image: Image): IntArray { + val bitmap = Bitmap.makeFromImage(image) + return IntArray(bitmap.width * bitmap.height) { bitmap.getColor(it % bitmap.width, it / bitmap.width) } + } + + fun inkOf(pixels: IntArray): Int = pixels.count { it != 0 } + + fun differing(a: IntArray, b: IntArray): Int { + check(a.size == b.size) { "frames of different sizes: ${a.size} vs ${b.size}" } + return a.indices.count { a[it] != b[it] } + } + + /** A Python string literal for [s]. */ + private fun pyLiteral(s: String): String = + "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'" +} diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookPracticeTest.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookPracticeTest.kt new file mode 100644 index 000000000..2076c805f --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookPracticeTest.kt @@ -0,0 +1,402 @@ +package fixture.notebook + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Card +import androidx.compose.material3.CardColors +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import fixture.notebook.NotebookHost.cell +import fixture.notebook.NotebookHost.inkOf +import fixture.notebook.NotebookRootTest.Companion.CELL_05 +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * The notebook's "UI 실습" section (cells 18-40): each practice cell redeclares the root, and the screen + * must then show exactly what the same composables draw from Kotlin. One test per cell, so a regression + * fails under the cell's number. + * + * Every cell is written in the spellings pythonx-compose decided, which differ from the notebook's text + * (pythonx-compose INTENT 2.4 and 5): `@app` instead of `main.App.update(...)`, Kotlin's own parameters + * snake-cased (`on_click`, `image_vector`, `tint`, `content_description`), `Color(0xFF...)` instead of a + * raw integer, `TextUnit(30, TextUnitType.Sp)` for a font size (a `TextUnit` refuses a raw number), + * `ButtonColors(...)` / `CardColors(...)` -- Kotlin's own classes -- for the notebook's `color=`, + * `Spacer(modifier=Modifier.padding(...))` instead of `Spacer(top=...)`, and `DefaultIcons.Add` read + * without parentheses. What the notebook writes and Kotlin has no counterpart for is listed in this + * module's README under "Gaps" and is left out of the cell rather than imitated. + */ +class NotebookPracticeTest { + + @BeforeTest + fun host() = NotebookHost.install() + + /** Cell 19: a red `Text`. */ + @Test + fun cell19_text() = assertCellDraws( + "19", + """ + from pythonx.compose.ui.graphics import Color + + @app + @Composable + def Practice(): + Text("나는 그냥 텍스트랍니다.", color=Color(0xFFFF0000)) + """, + ) { + Text("나는 그냥 텍스트랍니다.", color = Color(0xFFFF0000)) + } + + /** Cell 20: the same, at `font_size=30`. */ + @Test + fun cell20_textWithAFontSize() = assertCellDraws( + "20", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.ui.unit import TextUnit, TextUnitType + + @app + @Composable + def Practice(): + Text("나는 그냥 텍스트랍니다. 좀 더 커져봤어요.", color=Color(0xFFFF0000), font_size=TextUnit(30, TextUnitType.Sp)) + """, + ) { + Text("나는 그냥 텍스트랍니다. 좀 더 커져봤어요.", color = Color(0xFFFF0000), fontSize = 30.sp) + } + + /** Cell 22: a black `Button` with nothing inside. (`corner_radius=50` is a gap, see the README.) */ + @Test + fun cell22_anEmptyBlackButton() = assertCellDraws( + "22", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.material3 import ButtonColors + + @app + @Composable + def Practice(): # 내부 구성요소가 없는 버튼 + Button( + on_click=lambda: None, + colors=ButtonColors(container_color=Color(0xFF000000), content_color=Color(0xFFFFFFFF), + disabled_container_color=Color(0xFF000000), disabled_content_color=Color(0xFFFFFFFF)), + content=lambda: None, + ) + """, + ) { + Button(onClick = {}, colors = blackButton()) {} + } + + /** Cell 23: the same button, holding cell 20's text. */ + @Test + fun cell23_aButtonHoldingText() = assertCellDraws( + "23", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.ui.unit import TextUnit, TextUnitType + from pythonx.compose.material3 import ButtonColors + + @app + @Composable + def Practice(): # 텍스트가 하나 있는 버튼 + Button( + on_click=lambda: None, + colors=ButtonColors(container_color=Color(0xFF000000), content_color=Color(0xFFFFFFFF), + disabled_container_color=Color(0xFF000000), disabled_content_color=Color(0xFFFFFFFF)), + content=lambda: Text("나는 그냥 텍스트랍니다. 좀 더 커져봤어요.", color=Color(0xFFFF0000), font_size=TextUnit(30, TextUnitType.Sp)) + ) + """, + ) { + Button(onClick = {}, colors = blackButton()) { + Text("나는 그냥 텍스트랍니다. 좀 더 커져봤어요.", color = Color(0xFFFF0000), fontSize = 30.sp) + } + } + + /** Cell 25: a red `Card` with white text. (`modifier=modifier` and `corner_radius=20` are gaps.) */ + @Test + fun cell25_aCard() = assertCellDraws( + "25", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.material3 import CardColors + + @app + @Composable + def Practice(): + Card( + colors=CardColors(container_color=Color(0xFFFF0000), content_color=Color(0xFFFFFFFF), + disabled_container_color=Color(0xFFFF0000), disabled_content_color=Color(0xFFFFFFFF)), + content=lambda: Text("나는 그냥 텍스트랍니다.", color=Color(0xFFFFFFFF)) + ) + """, + ) { + Card(colors = CardColors(Color(0xFFFF0000), Color(0xFFFFFFFF), Color(0xFFFF0000), Color(0xFFFFFFFF))) { + Text("나는 그냥 텍스트랍니다.", color = Color(0xFFFFFFFF)) + } + } + + /** Cell 28: `DefaultIcons.Add` is Compose's `ImageVector` -- read, not called. */ + @Test + fun cell28_defaultIconsAddIsAnImageVector() { + cell(CELL_05) + cell( + """ + _nb_icon = DefaultIcons.Add + assert type(_nb_icon)._kotlin_type_name == 'androidx.compose.ui.graphics.vector.ImageVector', type(_nb_icon) + """, + ) + } + + /** Cell 29: a red `Add` icon. The notebook's `icon=`/`color=` are Kotlin's `image_vector=`/`tint=`. */ + @Test + fun cell29_anIcon() = assertCellDraws( + "29", + """ + from pythonx.compose.ui.graphics import Color + + @app + @Composable + def Practice(): # 아이콘 + Icon( + image_vector=DefaultIcons.Add, + content_description=None, + tint=Color(0xFFFF0000) + ) + """, + ) { + Icon(Icons.Default.Add, contentDescription = null, tint = Color(0xFFFF0000)) + } + + /** Cell 30: a yellow `Add` icon inside a black button. */ + @Test + fun cell30_anIconInsideAButton() = assertCellDraws( + "30", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.material3 import ButtonColors + + @app + @Composable + def Practice(): # 버튼 안에 아이콘 + Button( + on_click=lambda: None, + colors=ButtonColors(container_color=Color(0xFF000000), content_color=Color(0xFFFFFFFF), + disabled_container_color=Color(0xFF000000), disabled_content_color=Color(0xFFFFFFFF)), + content=lambda: { + Icon( + image_vector=DefaultIcons.Add, + content_description=None, + tint=Color(0xFFFFFF00) + ) + } + ) + """, + ) { + Button(onClick = {}, colors = blackButton()) { + Icon(Icons.Default.Add, contentDescription = null, tint = Color(0xFFFFFF00)) + } + } + + /** Cell 35: a `Column` of a small and a large button. */ + @Test + fun cell35_aColumnOfTwoButtons() = assertCellDraws( + "35", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.ui.unit import TextUnit, TextUnitType + from pythonx.compose.material3 import ButtonColors + + def _black(): + return ButtonColors(container_color=Color(0xFF000000), content_color=Color(0xFFFFFFFF), + disabled_container_color=Color(0xFF000000), disabled_content_color=Color(0xFFFFFFFF)) + + @app + @Composable + def Practice(): # 세로 정렬 + Column( + content=lambda: { + Button( + on_click=lambda: None, + colors=_black(), + content=lambda: Text("작은 버튼", color=Color(0xFFFFFFFF), font_size=TextUnit(10, TextUnitType.Sp)) + ), + Button( + on_click=lambda: None, + colors=_black(), + content=lambda: Text("큰 버튼", color=Color(0xFFFFFFFF), font_size=TextUnit(30, TextUnitType.Sp)) + ) + } + ) + """, + height = 200, + ) { + Column { + Button(onClick = {}, colors = blackButton()) { Text("작은 버튼", color = Color(0xFFFFFFFF), fontSize = 10.sp) } + Button(onClick = {}, colors = blackButton()) { Text("큰 버튼", color = Color(0xFFFFFFFF), fontSize = 30.sp) } + } + } + + /** Cell 36: a right-aligned `Column`, in the notebook's grouped spelling `Alignment.Horizontal.End`. */ + @Test + fun cell36_aColumnAlignedToTheEnd() = assertCellDraws( + "36", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.ui.unit import TextUnit, TextUnitType + + @app + @Composable + def Practice(): # 세로 정렬 + 오른쪽 정렬 + Column( + horizontal_alignment=Alignment.Horizontal.End, + content=lambda: { + Text("작은 글자", color=Color(0xFF000000), font_size=TextUnit(10, TextUnitType.Sp)), + Text("큰 글자", color=Color(0xFF000000), font_size=TextUnit(30, TextUnitType.Sp)) + } + ) + """, + ) { + Column(horizontalAlignment = Alignment.End) { + Text("작은 글자", color = Color(0xFF000000), fontSize = 10.sp) + Text("큰 글자", color = Color(0xFF000000), fontSize = 30.sp) + } + } + + /** + * Cell 37's signature: the notebook documents `Row(horizontal_arrangement=Arrangement.Center ...)`; + * no code cell passes one, so this is the one Arrangement scenario the notebook states. The row is + * given a width so that centring is visible. + */ + @Test + fun cell37_aRowWithAnArrangement() = assertCellDraws( + "37", + """ + @app + @Composable + def Practice(): + Row( + modifier=Modifier.width(300), + horizontal_arrangement=Arrangement.Center, + content=lambda: { + Text("작은 글자"), + Text("큰 글자") + } + ) + """, + ) { + Row(modifier = Modifier.width(300.dp), horizontalArrangement = Arrangement.Center) { + Text("작은 글자") + Text("큰 글자") + } + } + + /** Cell 38: a `Row` of a small and a large text. */ + @Test + fun cell38_aRow() = assertCellDraws( + "38", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.ui.unit import TextUnit, TextUnitType + + @app + @Composable + def Practice(): # 가로 정렬 + Row( + content=lambda: { + Text("작은 글자", color=Color(0xFF000000), font_size=TextUnit(10, TextUnitType.Sp)), + Text("큰 글자", color=Color(0xFF000000), font_size=TextUnit(30, TextUnitType.Sp)) + } + ) + """, + ) { + Row { + Text("작은 글자", color = Color(0xFF000000), fontSize = 10.sp) + Text("큰 글자", color = Color(0xFF000000), fontSize = 30.sp) + } + } + + /** Cell 40: cell 25's card again, its text spaced by `Spacer`s -- `Spacer(modifier=Modifier.padding(...))`. */ + @Test + fun cell40_aCardSpacedBySpacers() = assertCellDraws( + "40", + """ + from pythonx.compose.ui.graphics import Color + from pythonx.compose.material3 import CardColors + + @app + @Composable + def Practice(): # 아까 카드에 여백 넣어보기 + Card( + colors=CardColors(container_color=Color(0xFF000000), content_color=Color(0xFFFFFFFF), + disabled_container_color=Color(0xFF000000), disabled_content_color=Color(0xFFFFFFFF)), + content=lambda: { + Column( + content=lambda: { + Spacer(modifier=Modifier.padding(top=10)), + Row( + content=lambda: { + Spacer(modifier=Modifier.padding(start=20)), + Text("나는 그냥 텍스트랍니다.", color=Color(0xFFFFFFFF)), + Spacer(modifier=Modifier.padding(end=20)) + } + ), + Spacer(modifier=Modifier.padding(bottom=10)) + } + ) + } + ) + """, + ) { + Card(colors = CardColors(Color(0xFF000000), Color(0xFFFFFFFF), Color(0xFF000000), Color(0xFFFFFFFF))) { + Column { + Spacer(modifier = Modifier.padding(top = 10.dp)) + Row { + Spacer(modifier = Modifier.padding(start = 20.dp)) + Text("나는 그냥 텍스트랍니다.", color = Color(0xFFFFFFFF)) + Spacer(modifier = Modifier.padding(end = 20.dp)) + } + Spacer(modifier = Modifier.padding(bottom = 10.dp)) + } + } + } + + /** + * Runs cell 5 and then [source] against the live host, and requires the screen to equal [control] + * within [NotebookHost.MAX_FRAMES] frames. + */ + private fun assertCellDraws( + cellNumber: String, + source: String, + height: Int = NotebookRootTest.H, + control: @Composable () -> Unit, + ) { + val width = NotebookRootTest.W + val expected = NotebookHost.controlPixels(width, height, control) + assertTrue(inkOf(expected) > 0, "cell $cellNumber: the Kotlin control drew nothing, so the comparison measures nothing") + val scene = NotebookHost.hostScene(width, height) + try { + scene.render() + val seen = NotebookHost.redeclare(scene, CELL_05 + source.trimIndent(), expected) + println("notebook cell $cellNumber: ${seen.describe()}") + assertTrue(seen.matchesExpected, "cell $cellNumber: the screen is not what the cell declares: ${seen.describe()}") + } finally { + scene.close() + } + } +} + +private fun blackButton() = ButtonColors(Color(0xFF000000), Color(0xFFFFFFFF), Color(0xFF000000), Color(0xFFFFFFFF)) diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookRootTest.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookRootTest.kt new file mode 100644 index 000000000..9a2994439 --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookRootTest.kt @@ -0,0 +1,236 @@ +package fixture.notebook + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Density +import fixture.notebook.NotebookHost.cell +import fixture.notebook.NotebookHost.differing +import fixture.notebook.NotebookHost.framesUntil +import fixture.notebook.NotebookHost.inkOf +import fixture.notebook.NotebookHost.pixelsOf +import fixture.notebook.NotebookHost.pyInt +import fixture.notebook.NotebookHost.pyStr +import python.multiplatform.compose.PythonAppView +import python.multiplatform.compose.PythonWidget +import python.multiplatform.ffi.Python3 +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * The notebook's application-level scenarios (`UI.ipynb` cells 5-16): importing the app draws it, the + * notebook and the screen share state, and **redeclaring the root changes the screen with no update + * call** -- plus the negative checks that show the last one can fail. + * + * Every screen is compared pixel for pixel with the same composables drawn from Kotlin, so "the screen + * changed" means "the screen now shows what the cell declared", not merely "something moved". + */ +class NotebookRootTest { + + @BeforeTest + fun host() = NotebookHost.install() + + /** + * Cell 5's imports, in the spellings pythonx-compose decided (its INTENT section 5): everything the + * notebook imports that has a decided counterpart resolves against the installed wheel. The names it + * leaves out are the gaps in this module's README (`remember_saveable`, the coroutine scopes, the + * lower-case `modifier`). + */ + @Test + fun cell05_theImportsResolveFromTheInstalledWheel() { + cell(CELL_05) + cell( + """ + import pythonx.compose.material3 as _nb_m3 + assert DefaultIcons is not None and Icon is not None and TextField is not None + assert Column is _nb_m3.Column and Row is _nb_m3.Row and Spacer is _nb_m3.Spacer + assert Modifier is not None and Alignment is not None and Arrangement is not None + assert app is _nb_real_app and Composable(len) is len + """, + ) + } + + /** + * Cells 6-7: `import main` declares the app, and the host -- configured once with + * `PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")` -- draws it. `main.App` is the root on + * screen. An idle frame does not run it again: nothing polls. + */ + @Test + fun cell06to07_importingMainDrawsTheAppItDeclares() { + val expected = NotebookHost.controlPixels(W, H) { Text(INITIAL_MESSAGE) } + val scene = NotebookHost.hostScene(W, H) + try { + val first = pixelsOf(scene.render()) + assertEquals("True", pyStr("main.App is _nb_rt.app_root.value"), "cell 7: main.App is not the declared root") + assertEquals(1, pyInt("main.calls['App']"), "the declared root was not composed exactly once") + assertTrue(inkOf(expected) > 0, "the Kotlin control drew nothing, so the comparison measures nothing") + assertEquals(0, differing(first, expected), "main.App did not draw what Text(INITIAL_MESSAGE) draws from Kotlin") + + scene.render() + assertEquals(1, pyInt("main.calls['App']"), "an idle frame ran the root again, so something polls") + } finally { + scene.close() + } + } + + /** + * Cells 9-13: the notebook reads the state the screen shows (cell 10), writes it (cell 12) and the + * screen follows, then restores it (cell 13) and the screen is back -- with no call into the host. + * The root runs again because it read the state (a recomposition), not because anything was + * redeclared. The other direction, screen to notebook, is `NotebookTextFieldTest`. + */ + @Test + fun cell09to13_theNotebookAndTheScreenShareOneState() { + val initial = NotebookHost.controlPixels(W, H) { Text(INITIAL_MESSAGE) } + val changed = NotebookHost.controlPixels(W, H) { Text(CHANGED_MESSAGE) } + val scene = NotebookHost.hostScene(W, H) + try { + assertEquals(0, differing(pixelsOf(scene.render()), initial), "the app's first frame is not its initial message") + + // Cell 9: the state the root remembered is reachable as main.App.messages. + assertEquals("True", pyStr("hasattr(main.App, 'messages')"), "cell 9: main.App.messages does not exist after the first frame") + // Cell 10. + cell("message_backup = main.App.messages.getValue()") + assertEquals("", pyStr("type(message_backup)"), "the state did not read back as a str") + assertEquals(INITIAL_MESSAGE, pyStr("message_backup"), "cell 10 read something other than what is on screen") + + // Cell 12. + cell("main.App.messages.setValue('$CHANGED_MESSAGE')") + val (afterWrite, writeFrames) = framesUntil({ scene.render() }) { it.contentEquals(changed) } + assertEquals( + 0, differing(afterWrite, changed), + "cell 12: after $writeFrames frame(s) the screen does not show the message the notebook wrote", + ) + assertEquals(2, pyInt("main.calls['App']"), "the write did not recompose the root exactly once") + + // Cell 13. + cell("main.App.messages.setValue(message_backup) # 복원") + val (afterRestore, restoreFrames) = framesUntil({ scene.render() }) { it.contentEquals(initial) } + assertEquals( + 0, differing(afterRestore, initial), + "cell 13: after $restoreFrames frame(s) the screen does not show the restored message", + ) + assertEquals(INITIAL_MESSAGE, pyStr("main.App.messages.getValue()")) + assertEquals(INITIAL_MESSAGE, pyStr("main.App.messages.value"), ".value is not an alias of getValue()") + } finally { + scene.close() + } + } + + /** + * Cells 15-16, the scenario issue #26 is about: a cell redeclares the root and the screen shows it. + * The notebook's `main.App.update(NewUI)` is gone (pythonx-compose INTENT 5.1); `@app` on the new + * function is the whole of it. Between the cell and the frame the test does only what a frame clock + * does ([NotebookHost.nextFrame]). + */ + @Test + fun cell15to16_redeclaringTheRootChangesTheScreenWithNoUpdateCall() { + val expected = NotebookHost.controlPixels(W, H) { NewUiControl() } + val scene = NotebookHost.hostScene(W, H) + try { + scene.render() + val seen = NotebookHost.redeclare(scene, CELL_05 + CELL_15, expected) + println("notebook cell 15: ${seen.describe()}") + assertTrue(inkOf(expected) > 0, "the Kotlin control drew nothing") + assertTrue(seen.matchesExpected, "cell 15: the redeclared root is not on screen: ${seen.describe()}") + assertEquals("True", pyStr("_nb_rt.app_root.value is NewUI"), "@app did not make NewUI the root") + assertEquals(1, pyInt("main.calls['App']"), "the replaced root ran again") + } finally { + scene.close() + } + } + + /** + * **Negative check, Python half.** `@app` replaced by a stub that returns the function without + * writing `app_root` -- the root-replacement wiring disabled on the pythonx side. The same + * observation [cell15to16_redeclaringTheRootChangesTheScreenWithNoUpdateCall] asserts on must now + * report that nothing changed; if it did not, that test could not fail. + */ + @Test + fun negative_withAppStubbedOutTheRedeclarationDoesNotReachTheScreen() { + val expected = NotebookHost.controlPixels(W, H) { NewUiControl() } + val scene = NotebookHost.hostScene(W, H) + try { + scene.render() + cell("_nb_rt.app = lambda root: root") + val seen = NotebookHost.redeclare(scene, CELL_05 + CELL_15, expected) + println("notebook negative (stubbed @app): ${seen.describe()}") + assertFalse(seen.matchesExpected, "with @app stubbed out the screen still showed the redeclared root") + assertEquals(0, seen.changedPixels, "with @app stubbed out the screen changed anyway: ${seen.describe()}") + } finally { + scene.close() + } + } + + /** + * **Negative check, host half.** The host given the root *function* once (`PythonWidget(root = + * main.App)`) instead of the state that holds it -- the root-replacement wiring disabled on the + * Kotlin side. A redeclaration then writes a state no composition reads, and the screen stays. + */ + @Test + fun negative_aHostHandedTheRootOnceDoesNotFollowARedeclaration() { + val expected = NotebookHost.controlPixels(W, H) { NewUiControl() } + val fixedRoot = Python3.import("main").getAttr("App") + val scene = ImageComposeScene(width = W, height = H, density = Density(1f)) { PythonWidget(root = fixedRoot) } + try { + scene.render() + val seen = NotebookHost.redeclare(scene, CELL_05 + CELL_15, expected) + println("notebook negative (fixed root): ${seen.describe()}") + assertFalse(seen.matchesExpected, "a host that never reads app_root still showed the redeclared root") + assertEquals(0, seen.changedPixels, "a host that never reads app_root changed anyway: ${seen.describe()}") + assertEquals("True", pyStr("_nb_rt.app_root.value is NewUI"), "the redeclaration itself did not happen") + } finally { + scene.close() + fixedRoot.close() + } + } + + internal companion object { + const val W = 360 + const val H = 120 + + /** `main.py`'s `INITIAL_MESSAGE`. */ + const val INITIAL_MESSAGE = "안녕하세요, Python 앱입니다." + + /** Cell 12's text. */ + const val CHANGED_MESSAGE = "UI 상태를 변경해보는 테스트입니다." + + /** + * Cell 5 in the decided spellings: `app` beside `Composable`; no `remember_saveable`, + * `DefaultCoroutineScope`, `MainCoroutineScope` (undecided, pythonx-compose INTENT 4.1 and SPEC + * section 8) and no lower-case `modifier` (INTENT 5.4). + */ + val CELL_05 = """ + from pythonx.compose.runtime import Composable, app + from pythonx.compose.material3 import Icon, DefaultIcons, Text, TextField + from pythonx.compose.material3 import Column, Row, Button, Card, Spacer + from pythonx.compose.ui import Modifier, Alignment + from pythonx.compose.layout import Arrangement + + """.trimIndent() + "\n" + + /** Cells 15-16: `NewUI`, declared with `@app` instead of `main.App.update(NewUI)`; `color=` is `Color(...)` (INTENT 5.5). */ + val CELL_15 = """ + from pythonx.compose.ui.graphics import Color + + @app + @Composable + def NewUI(): + Button( + on_click=lambda: None, + content=lambda: { + Text(f"버튼이 하나만 있는 화면으로 바꿔보자", color=Color(0xFFFFFFFF)) + } + ) + """.trimIndent() + } +} + +@androidx.compose.runtime.Composable +private fun NewUiControl() { + Button(onClick = {}) { Text("버튼이 하나만 있는 화면으로 바꿔보자", color = Color(0xFFFFFFFF)) } +} diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookTextFieldTest.kt b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookTextFieldTest.kt new file mode 100644 index 000000000..fb417526f --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/kotlin/fixture/notebook/NotebookTextFieldTest.kt @@ -0,0 +1,163 @@ +@file:OptIn(ExperimentalFoundationApi::class) + +package fixture.notebook + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.TextRange +import fixture.notebook.NotebookHost.cell +import fixture.notebook.NotebookHost.differing +import fixture.notebook.NotebookHost.framesUntil +import fixture.notebook.NotebookHost.inkOf +import fixture.notebook.NotebookHost.nextFrame +import fixture.notebook.NotebookHost.pixelsOf +import fixture.notebook.NotebookHost.pyInt +import fixture.notebook.NotebookHost.pyStr +import fixture.notebook.NotebookRootTest.Companion.CELL_05 +import python.multiplatform.compose.PythonAppView +import python.multiplatform.reflection.HandleTable +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +/** + * Cells 31-33: the text field. The screen writes, the notebook reads (cell 33, "화면에서 값을 변경한 후 + * 출력해보세요" -- change it on screen, then print it). + * + * In the decided spelling (pythonx-compose INTENT 5.8, SPEC S5.5) the field is `TextField(state=...)` + * over a `TextFieldState` from `remember_text_field_state("")`, so Compose owns the text buffer and the + * input method's composing region and **nothing crosses into Python per keystroke**. This is + * pythonx-compose #10's completion criterion, checked here against a real composition: + * + * 1. The cell declares the field; the host draws it; a click focuses it and it opens an input session. + * 2. A Hangul input-method sequence -- ㅎ, 하, 한 composing, then 한 committed -- is delivered the way the + * desktop window delivers one ([InputMethodScene.inputMethodEvent]), a frame after each event. A + * counter of every Python function started ([NotebookHost.pythonFunctionsStartedDuring]) must stay at + * zero across all of it, and the root must not run again. + * 3. Kotlin's own `TextFieldState` holds the composing text with its composing range during the + * sequence, and the committed text with no composing range after it; the notebook then reads + * `field.text == "한"`. + * 4. Outside the composition, `field.set_text_and_place_cursor_at_end("x")` reaches the screen within + * [NotebookHost.MAX_FRAMES] frames. + */ +class NotebookTextFieldTest { + + @BeforeTest + fun host() = NotebookHost.install() + + @Test + fun cell32to33_aTextFieldKeepsInputMethodCompositionInComposeAndTheNotebookReadsTheResult() { + val scene = InputMethodScene(WIDTH, HEIGHT) { + PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root") + } + try { + scene.render() + cell(CELL_05 + CELL_32) + framesUntil({ scene.render() }) { pyInt("len(fields)") > 0 } + assertTrue(pyInt("len(fields)") > 0, "cell 32: the redeclared Practice root never composed") + val state = HandleTable.resolveRaw(pyStr("int(fields[-1]._pm_handle)").toLong()) as? TextFieldState + ?: fail("fields[-1] is not a Kotlin TextFieldState: ${pyStr("type(fields[-1])")}") + assertEquals("", state.text.toString()) + + // 1. Focus, by a click on the field; keyboard traversal if the click did not do it. + scene.click(Offset(FIELD_CENTRE_X, FIELD_CENTRE_Y)) + framesUntil({ scene.render() }) { scene.activeRequest != null } + var focusedBy = "click" + if (scene.activeRequest == null) { + focusedBy = "focus traversal (the click did not focus the field)" + scene.focusFirst() + framesUntil({ scene.render() }) { scene.activeRequest != null } + } + if (scene.activeRequest == null) fail("the TextField never opened a text input session, so no input method can reach it") + + // 2-3. The Hangul sequence, with the Python-function counter on. + val practiceRuns = pyInt("practice_calls[0]") + val observed = ArrayList>() + val started = NotebookHost.pythonFunctionsStartedDuring { + for ((committed, composing) in HANGUL_EVENTS) { + scene.inputMethodEvent(committed, composing) + nextFrame { scene.render() } + observed += state.text.toString() to state.composition + } + } + println("notebook cell 32: focused by $focusedBy; IME states $observed; Python functions started: ${started.size}") + assertEquals( + emptyList(), started, + "Python ran while the input method typed into TextField(state=...); every key should stay in Compose", + ) + assertEquals(practiceRuns, pyInt("practice_calls[0]"), "typing re-ran the Practice root") + assertEquals( + listOf( + "ㅎ" to TextRange(0, 1), + "하" to TextRange(0, 1), + "한" to TextRange(0, 1), + "한" to null, + ), + observed, + "the TextFieldState did not hold the composing text and its range, then the committed text", + ) + + // Cell 33: the notebook reads what the screen wrote. + assertEquals("", pyStr("type(fields[-1].text)")) + assertEquals("한", pyStr("fields[-1].text"), "cell 33: the notebook does not read what was typed on screen") + assertEquals("True", pyStr("all(_f.text == '한' for _f in fields)"), "the root saw more than one state") + + // 4. Written from the notebook, outside the composition. + val before = pixelsOf(scene.render()) + cell("fields[-1].set_text_and_place_cursor_at_end('x')") + val (after, frames) = framesUntil({ scene.render() }) { differing(before, it) > 10 } + println("notebook cell 32: set_text_and_place_cursor_at_end moved ${differing(before, after)} px after $frames frame(s)") + assertEquals("x", state.text.toString(), "the write from Python did not reach the Kotlin state") + assertTrue(inkOf(before) > 0, "the field drew nothing") + assertTrue( + differing(before, after) > 10, + "the text written from the notebook did not reach the screen within ${NotebookHost.MAX_FRAMES} frames", + ) + } finally { + scene.close() + } + } + + private companion object { + const val WIDTH = 320 + const val HEIGHT = 80 + + /** Inside material3's `TextField` (280 x 56 by default), drawn at `padding(8)`. */ + const val FIELD_CENTRE_X = 148f + const val FIELD_CENTRE_Y = 36f + + /** (committed, composing) per input-method event: a Hangul IME typing 한 (ㅎ, ㅏ, ㄴ). */ + val HANGUL_EVENTS = listOf( + "" to "ㅎ", + "" to "하", + "" to "한", + "한" to "", + ) + + /** + * Cell 32 in the decided spelling: `TextField(state=..., modifier=Modifier.padding(8))` over a + * `remember_text_field_state("")`, instead of `TextField(text_state=main.App.messages, padding=8)`. + * `fields` and `practice_calls` are the test's handles on what the root made and how often it ran. + */ + val CELL_32 = """ + from pythonx.compose.foundation.text.input import remember_text_field_state + + fields = [] + practice_calls = [0] + + @app + @Composable + def Practice(): # 텍스트 입력란 + practice_calls[0] += 1 + field = remember_text_field_state("") + fields.append(field) + TextField( + state=field, + modifier=Modifier.padding(8) + ) + """.trimIndent() + } +} diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/native-image-main/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/native-image-main/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory new file mode 100644 index 000000000..b12a346d1 --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/native-image-main/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory @@ -0,0 +1 @@ +fixture.notebook.NotebookE2eMainDispatcherFactory diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/native-image/reachability-metadata.json b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/native-image/reachability-metadata.json new file mode 100644 index 000000000..3439d77ec --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/native-image/reachability-metadata.json @@ -0,0 +1,1448 @@ +{ + "reflection": [ + { + "type": "android.os.Build" + }, + { + "type": "fixture.notebook.NotebookE2eMainDispatcherFactory", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "type": "java.io.OutputStream", + "jniAccessible": true, + "methods": [ + { + "name": "flush", + "parameterTypes": [] + }, + { + "name": "write", + "parameterTypes": [ + "byte[]", + "int", + "int" + ] + } + ] + }, + { + "type": "java.lang.Boolean", + "jniAccessible": true, + "methods": [ + { + "name": "booleanValue", + "parameterTypes": [] + }, + { + "name": "getBoolean", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "java.lang.Float", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "float" + ] + } + ] + }, + { + "type": "java.lang.RuntimeException", + "jniAccessible": true + }, + { + "type": "java.lang.String", + "jniAccessible": true + }, + { + "type": "java.lang.System", + "jniAccessible": true, + "methods": [ + { + "name": "getProperty", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "java.lang.Throwable", + "jniAccessible": true, + "methods": [ + { + "name": "printStackTrace", + "parameterTypes": [] + } + ] + }, + { + "type": "java.lang.foreign.Arena", + "methods": [ + { + "name": "allocateFrom", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "global", + "parameterTypes": [] + } + ] + }, + { + "type": "java.lang.foreign.FunctionDescriptor", + "methods": [ + { + "name": "of", + "parameterTypes": [ + "java.lang.foreign.MemoryLayout", + "java.lang.foreign.MemoryLayout[]" + ] + }, + { + "name": "ofVoid", + "parameterTypes": [ + "java.lang.foreign.MemoryLayout[]" + ] + } + ] + }, + { + "type": "java.lang.foreign.Linker", + "methods": [ + { + "name": "defaultLookup", + "parameterTypes": [] + }, + { + "name": "downcallHandle", + "parameterTypes": [ + "java.lang.foreign.FunctionDescriptor", + "java.lang.foreign.Linker$Option[]" + ] + }, + { + "name": "downcallHandle", + "parameterTypes": [ + "java.lang.foreign.MemorySegment", + "java.lang.foreign.FunctionDescriptor", + "java.lang.foreign.Linker$Option[]" + ] + }, + { + "name": "nativeLinker", + "parameterTypes": [] + }, + { + "name": "upcallStub", + "parameterTypes": [ + "java.lang.invoke.MethodHandle", + "java.lang.foreign.FunctionDescriptor", + "java.lang.foreign.Arena", + "java.lang.foreign.Linker$Option[]" + ] + } + ] + }, + { + "type": "java.lang.foreign.Linker$Option" + }, + { + "type": "java.lang.foreign.Linker$Option[]" + }, + { + "type": "java.lang.foreign.MemoryLayout" + }, + { + "type": "java.lang.foreign.MemoryLayout[]" + }, + { + "type": "java.lang.foreign.MemorySegment", + "fields": [ + { + "name": "NULL" + } + ], + "methods": [ + { + "name": "address", + "parameterTypes": [] + }, + { + "name": "copy", + "parameterTypes": [ + "java.lang.foreign.MemorySegment", + "long", + "java.lang.foreign.MemorySegment", + "long", + "long" + ] + }, + { + "name": "getString", + "parameterTypes": [ + "long" + ] + }, + { + "name": "ofAddress", + "parameterTypes": [ + "long" + ] + }, + { + "name": "ofArray", + "parameterTypes": [ + "byte[]" + ] + }, + { + "name": "reinterpret", + "parameterTypes": [ + "long" + ] + } + ] + }, + { + "type": "java.lang.foreign.SymbolLookup", + "methods": [ + { + "name": "find", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "loaderLookup", + "parameterTypes": [] + }, + { + "name": "or", + "parameterTypes": [ + "java.lang.foreign.SymbolLookup" + ] + } + ] + }, + { + "type": "java.lang.foreign.ValueLayout", + "fields": [ + { + "name": "ADDRESS" + }, + { + "name": "JAVA_BYTE" + }, + { + "name": "JAVA_DOUBLE" + }, + { + "name": "JAVA_FLOAT" + }, + { + "name": "JAVA_INT" + }, + { + "name": "JAVA_LONG" + }, + { + "name": "JAVA_SHORT" + } + ] + }, + { + "type": "java.util.Iterator", + "jniAccessible": true, + "methods": [ + { + "name": "hasNext", + "parameterTypes": [] + }, + { + "name": "next", + "parameterTypes": [] + } + ] + }, + { + "type": "kotlin.coroutines.SafeContinuation" + }, + { + "type": "kotlin.jvm.functions.Function0", + "jniAccessible": true, + "methods": [ + { + "name": "invoke", + "parameterTypes": [] + } + ] + }, + { + "type": "kotlin.reflect.jvm.internal.ReflectionFactoryImpl" + }, + { + "type": "kotlinx.atomicfu.AtomicInt" + }, + { + "type": "kotlinx.atomicfu.AtomicRef" + }, + { + "type": "kotlinx.coroutines.CancellableContinuationImpl" + }, + { + "type": "kotlinx.coroutines.CancelledContinuation" + }, + { + "type": "kotlinx.coroutines.CompletedExceptionally" + }, + { + "type": "kotlinx.coroutines.EventLoopImplBase" + }, + { + "type": "kotlinx.coroutines.JobSupport" + }, + { + "type": "kotlinx.coroutines.JobSupport$Finishing" + }, + { + "type": "kotlinx.coroutines.channels.BufferedChannel" + }, + { + "type": "kotlinx.coroutines.flow.StateFlowImpl" + }, + { + "type": "kotlinx.coroutines.internal.ConcurrentLinkedListNode" + }, + { + "type": "kotlinx.coroutines.internal.DispatchedContinuation" + }, + { + "type": "kotlinx.coroutines.internal.LimitedDispatcher" + }, + { + "type": "kotlinx.coroutines.internal.LockFreeLinkedListNode" + }, + { + "type": "kotlinx.coroutines.internal.LockFreeTaskQueue" + }, + { + "type": "kotlinx.coroutines.internal.LockFreeTaskQueueCore" + }, + { + "type": "kotlinx.coroutines.internal.Segment" + }, + { + "type": "kotlinx.coroutines.internal.ThreadSafeHeap" + }, + { + "type": "kotlinx.coroutines.scheduling.CoroutineScheduler" + }, + { + "type": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker" + }, + { + "type": "kotlinx.coroutines.scheduling.WorkQueue" + }, + { + "type": "kotlinx.coroutines.sync.MutexImpl" + }, + { + "type": "kotlinx.coroutines.sync.SemaphoreAndMutexImpl" + }, + { + "type": "org.jetbrains.skia.AnimationFrameInfo", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "int", + "boolean", + "int", + "boolean", + "int", + "int", + "org.jetbrains.skia.IRect" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.Color4f", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.Drawable", + "jniAccessible": true, + "methods": [ + { + "name": "_onDraw", + "parameterTypes": [ + "long" + ] + }, + { + "name": "onGetBounds", + "parameterTypes": [] + } + ] + }, + { + "type": "org.jetbrains.skia.FontFamilyName", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.String", + "java.lang.String" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.FontFeature", + "jniAccessible": true, + "fields": [ + { + "name": "_tag" + }, + { + "name": "end" + }, + { + "name": "start" + }, + { + "name": "value" + } + ], + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.String", + "int" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.FontMetrics", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "java.lang.Float", + "java.lang.Float", + "java.lang.Float", + "java.lang.Float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.FontVariation", + "jniAccessible": true, + "fields": [ + { + "name": "_tag" + }, + { + "name": "value" + } + ], + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.FontVariationAxis", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "float", + "float", + "float", + "boolean" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.IPoint", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "int" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.IRect", + "jniAccessible": true, + "fields": [ + { + "name": "bottom" + }, + { + "name": "left" + }, + { + "name": "right" + }, + { + "name": "top" + } + ], + "methods": [ + { + "name": "makeLTRB", + "parameterTypes": [ + "int", + "int", + "int", + "int" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.ImageInfo", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "long" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.PaintFilterCanvas", + "jniAccessible": true, + "methods": [ + { + "name": "onFilter", + "parameterTypes": [ + "long" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.Path", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "long" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.PathSegment", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "boolean" + ] + }, + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "boolean" + ] + }, + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float", + "float", + "boolean" + ] + }, + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "boolean", + "boolean" + ] + }, + { + "name": "", + "parameterTypes": [ + "int", + "float", + "float", + "boolean" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.Point", + "jniAccessible": true, + "fields": [ + { + "name": "x" + }, + { + "name": "y" + } + ], + "methods": [ + { + "name": "", + "parameterTypes": [ + "float", + "float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.RRect", + "jniAccessible": true, + "fields": [ + { + "name": "radii" + } + ], + "methods": [ + { + "name": "makeComplexLTRB", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float[]" + ] + }, + { + "name": "makeLTRB", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float" + ] + }, + { + "name": "makeLTRB", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float", + "float" + ] + }, + { + "name": "makeLTRB", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "float" + ] + }, + { + "name": "makeNinePatchLTRB", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "float", + "float", + "float", + "float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.RSXform", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.Rect", + "jniAccessible": true, + "fields": [ + { + "name": "bottom" + }, + { + "name": "left" + }, + { + "name": "right" + }, + { + "name": "top" + } + ], + "methods": [ + { + "name": "makeLTRB", + "parameterTypes": [ + "float", + "float", + "float", + "float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.impl.Native", + "jniAccessible": true, + "fields": [ + { + "name": "_ptr" + } + ] + }, + { + "type": "org.jetbrains.skia.paragraph.DecorationStyle", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "boolean", + "boolean", + "boolean", + "boolean", + "int", + "int", + "float" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.paragraph.LineMetrics", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "boolean", + "double", + "double", + "double", + "double", + "double", + "double", + "double", + "int" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.paragraph.LineMetrics[]" + }, + { + "type": "org.jetbrains.skia.paragraph.Shadow", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "float", + "float", + "double" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.paragraph.TextBox", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "float", + "float", + "float", + "float", + "int" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.paragraph.TextBox[]" + }, + { + "type": "org.jetbrains.skia.shaper.BidiRun", + "jniAccessible": true, + "fields": [ + { + "name": "end" + }, + { + "name": "level" + } + ] + }, + { + "type": "org.jetbrains.skia.shaper.FontMgrRunIterator", + "jniAccessible": true + }, + { + "type": "org.jetbrains.skia.shaper.FontRun", + "jniAccessible": true, + "fields": [ + { + "name": "end" + } + ], + "methods": [ + { + "name": "_getFontPtr", + "parameterTypes": [] + } + ] + }, + { + "type": "org.jetbrains.skia.shaper.HbIcuScriptRunIterator", + "jniAccessible": true + }, + { + "type": "org.jetbrains.skia.shaper.IcuBidiRunIterator", + "jniAccessible": true + }, + { + "type": "org.jetbrains.skia.shaper.LanguageRun", + "jniAccessible": true, + "fields": [ + { + "name": "end" + }, + { + "name": "language" + } + ] + }, + { + "type": "org.jetbrains.skia.shaper.RunHandler", + "jniAccessible": true, + "methods": [ + { + "name": "beginLine", + "parameterTypes": [] + }, + { + "name": "commitLine", + "parameterTypes": [] + }, + { + "name": "commitRun", + "parameterTypes": [ + "org.jetbrains.skia.shaper.RunInfo", + "short[]", + "org.jetbrains.skia.Point[]", + "int[]" + ] + }, + { + "name": "commitRunInfo", + "parameterTypes": [] + }, + { + "name": "runInfo", + "parameterTypes": [ + "org.jetbrains.skia.shaper.RunInfo" + ] + }, + { + "name": "runOffset", + "parameterTypes": [ + "org.jetbrains.skia.shaper.RunInfo" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.shaper.RunInfo", + "jniAccessible": true, + "fields": [ + { + "name": "_fontPtr" + } + ], + "methods": [ + { + "name": "", + "parameterTypes": [ + "long", + "int", + "float", + "float", + "int", + "int", + "int" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.shaper.ScriptRun", + "jniAccessible": true, + "fields": [ + { + "name": "end" + }, + { + "name": "scriptTag" + } + ] + }, + { + "type": "org.jetbrains.skia.shaper.ShapingOptions", + "jniAccessible": true, + "fields": [ + { + "name": "features" + }, + { + "name": "fontMgr" + }, + { + "name": "isApproximatePunctuation" + }, + { + "name": "isApproximateSpaces" + }, + { + "name": "isLeftToRight" + } + ] + }, + { + "type": "org.jetbrains.skia.shaper.TextBlobBuilderRunHandler", + "jniAccessible": true + }, + { + "type": "org.jetbrains.skia.skottie.LogLevel", + "jniAccessible": true, + "fields": [ + { + "name": "ERROR" + }, + { + "name": "WARNING" + } + ] + }, + { + "type": "org.jetbrains.skia.skottie.Logger", + "jniAccessible": true, + "methods": [ + { + "name": "log", + "parameterTypes": [ + "org.jetbrains.skia.skottie.LogLevel", + "java.lang.String", + "java.lang.String" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.svg.SVGLength", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "float", + "int" + ] + } + ] + }, + { + "type": "org.jetbrains.skia.svg.SVGPreserveAspectRatio", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [ + "int", + "int" + ] + } + ] + }, + { + "type": "python.multiplatform.ffi.ProxyType", + "methods": [ + { + "name": "tp_clear", + "parameterTypes": [ + "long" + ] + }, + { + "name": "tp_dealloc", + "parameterTypes": [ + "long" + ] + }, + { + "name": "tp_traverse", + "parameterTypes": [ + "long", + "long", + "long" + ] + } + ] + }, + { + "type": "python.native.ffi.Panama", + "methods": [ + { + "name": "modernLongToSegment", + "parameterTypes": [ + "long" + ] + } + ] + }, + { + "type": "python.native.ffi.UpcallTarget", + "methods": [ + { + "name": "upcallCancelCall", + "parameterTypes": [ + "long" + ] + }, + { + "name": "upcallInvokeWithArgs", + "parameterTypes": [ + "long", + "long" + ] + }, + { + "name": "upcallReleaseObject", + "parameterTypes": [ + "long" + ] + }, + { + "name": "upcallResolveHandle", + "parameterTypes": [ + "long" + ] + } + ] + } + ], + "resources": [ + { + "glob": "META-INF/services/kotlin.test.AsserterContributor" + }, + { + "glob": "META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory" + }, + { + "glob": "lib/macos-aarch64/libpython3.15.dylib" + }, + { + "glob": "libskiko-macos-arm64.dylib.sha256" + }, + { + "glob": "python" + }, + { + "module": "java.base", + "glob": "jdk/internal/icu/impl/data/icudt76b/nfc.nrm" + } + ], + "foreign": { + "downcalls": [ + { + "returnType": "void", + "parameterTypes": [ + "jint" + ] + }, + { + "returnType": "jint", + "parameterTypes": [ + "jlong" + ] + }, + { + "returnType": "jint", + "parameterTypes": [ + "jint" + ] + }, + { + "returnType": "jint", + "parameterTypes": [ + "jlong", + "jlong", + "jlong" + ] + }, + { + "returnType": "jint", + "parameterTypes": [ + "jlong", + "jlong", + "jint" + ] + }, + { + "returnType": "jdouble", + "parameterTypes": [] + }, + { + "returnType": "void", + "parameterTypes": [ + "jlong", + "jint", + "jint" + ] + }, + { + "returnType": "void", + "parameterTypes": [ + "jlong", + "jlong" + ] + }, + { + "returnType": "void", + "parameterTypes": [ + "jlong", + "jint" + ] + }, + { + "returnType": "void", + "parameterTypes": [] + }, + { + "returnType": "void", + "parameterTypes": [ + "jlong", + "jlong", + "jlong" + ] + }, + { + "returnType": "void", + "parameterTypes": [ + "jlong" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jint" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jdouble" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong", + "jlong", + "jlong", + "jlong", + "jlong" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong", + "jlong", + "jlong" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jint", + "jlong", + "jlong" + ] + }, + { + "returnType": "jint", + "parameterTypes": [ + "jlong", + "jlong" + ] + }, + { + "returnType": "jdouble", + "parameterTypes": [ + "jlong" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong", + "jlong", + "jlong", + "jlong" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong", + "jlong", + "jlong", + "jint" + ] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong", + "jlong" + ] + }, + { + "returnType": "jint", + "parameterTypes": [] + }, + { + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong", + "jint" + ] + }, + { + "returnType": "jint", + "parameterTypes": [ + "jlong", + "jlong", + "jlong", + "jint", + "jlong", + "jlong" + ] + } + ], + "directUpcalls": [ + { + "class": "python.multiplatform.ffi.ProxyType", + "method": "tp_clear", + "returnType": "jint", + "parameterTypes": [ + "jlong" + ] + }, + { + "class": "python.native.ffi.UpcallTarget", + "method": "upcallCancelCall", + "returnType": "jint", + "parameterTypes": [ + "jlong" + ] + }, + { + "class": "python.multiplatform.ffi.ProxyType", + "method": "tp_traverse", + "returnType": "jint", + "parameterTypes": [ + "jlong", + "jlong", + "jlong" + ] + }, + { + "class": "python.multiplatform.ffi.ProxyType", + "method": "tp_dealloc", + "returnType": "void", + "parameterTypes": [ + "jlong" + ] + }, + { + "class": "python.native.ffi.UpcallTarget", + "method": "upcallReleaseObject", + "returnType": "jint", + "parameterTypes": [ + "jlong" + ] + }, + { + "class": "python.native.ffi.UpcallTarget", + "method": "upcallInvokeWithArgs", + "returnType": "jlong", + "parameterTypes": [ + "jlong", + "jlong" + ] + }, + { + "class": "python.native.ffi.UpcallTarget", + "method": "upcallResolveHandle", + "returnType": "jlong", + "parameterTypes": [ + "jlong" + ] + } + ] + } +} \ No newline at end of file diff --git a/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/python/main.py b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/python/main.py new file mode 100644 index 000000000..1d56282b2 --- /dev/null +++ b/python-multiplatform-compose/fixtures/notebook-e2e/src/desktopTest/python/main.py @@ -0,0 +1,25 @@ +"""The application module `UI.ipynb` imports as `main` (cell 6), written with pythonx-compose's API. + +It follows the notebook's own spelling, as pythonx-compose ships it: the screen is *declared* with +`@app` (there is no `main.App.update(...)`), and the UI state is the root's `remember_saveable` +value, which the app attaches to the function itself (`App.messages`), so the notebook reads and +writes it as `main.App.messages.getValue()` / `setValue(...)` (cells 9-13). + +`calls` counts how often each root ran, so a test can tell a recomposition from a redeclaration and +an idle frame from a poll. It is the test's instrument, not part of the notebook. +""" + +from pythonx.compose.runtime import Composable, app, remember_saveable +from pythonx.compose.material3 import Text + +INITIAL_MESSAGE = "안녕하세요, Python 앱입니다." + +calls = {"App": 0} + + +@app +@Composable +def App(): + calls["App"] += 1 + App.messages = messages = remember_saveable(INITIAL_MESSAGE) + Text(messages.getValue()) diff --git a/python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/PythonContent.kt b/python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/PythonAppView.kt similarity index 58% rename from python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/PythonContent.kt rename to python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/PythonAppView.kt index 8de24e3e6..59cb1622c 100644 --- a/python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/PythonContent.kt +++ b/python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/PythonAppView.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.State import androidx.compose.runtime.key import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout import python.multiplatform.ffi.PyObject import python.multiplatform.ffi.Python3 import python.multiplatform.ffi.pythonx.PythonCallables @@ -12,7 +14,70 @@ import python.multiplatform.reflection.HandleTable import python.native.ffi.PyLong_AsLongLong /** - * Composes the Python application root found at [root], and recomposes whenever Python replaces it. + * Starts the Python interpreter if it is not running yet, then composes [content]. + * + * This is the first of the three host composables (`PythonLauncher`, [PythonWidget], [PythonAppView]) + * and its job is the interpreter start only: wrap the part of the UI that reaches Python in it. + * Registering the composer with the Python runtime is **not** done here once for the whole tree: it + * is done per Python call, by [withPythonComposer], each time a root or widget is composed, so that + * the composer a bound `@Composable` sees is always the one of the composition that called it. + */ +@Composable +fun PythonLauncher(content: @Composable () -> Unit) { + if (!Python3.isInitialized) { + Python3.initialize() + } + content() +} + +/** + * The module [PythonAppView] and [PythonWidget] look in when none is named: Python's `__main__`. + */ +const val DEFAULT_PYTHON_MODULE: String = "__main__" + +/** + * Composes the Python application root found at `module.attribute`, and recomposes whenever Python + * replaces it. The declarative app root: `PythonAppView(module = "main", attribute = "App")` for a + * Python `main.App`. + * + * `attribute` is a Python callable taking no arguments, or a Compose [State] that Python holds whose + * value is such a callable (or `None` for "nothing yet"); see [PythonAppView] with a `root` for the + * semantics. The attribute is resolved **once**, when this enters the composition (and again only if + * [module] or [attribute] change). Rebinding the module attribute afterwards is not observed; + * replacing the root is a write into the state the attribute names, which is. + * + * [modifier] sizes and places the area the root draws into. The original design's `Surface` and + * colour parameters are left out on purpose: that needs material3, which this module does not depend + * on. Wrap the call in a `Surface` where one is wanted. + */ +@Composable +fun PythonAppView( + modifier: Modifier = Modifier, + module: String = DEFAULT_PYTHON_MODULE, + attribute: String = "App", +) { + val resolved = remember(module, attribute) { ResolvedAttribute(module, attribute) } + PythonAppView(resolved.value, modifier) +} + +/** + * Composes the named Python composable `moduleName.composableName`: a callable taking no arguments, + * or a Compose [State] that Python holds whose value is one. Same semantics as [PythonAppView] with + * a `root`. Unlike the original design, no Kotlin `content` slot is passed to the callable, and the + * `Surface` parameters are left out (see [PythonAppView]). + */ +@Composable +fun PythonWidget( + composableName: String, + modifier: Modifier = Modifier, + moduleName: String = DEFAULT_PYTHON_MODULE, +) { + val resolved = remember(moduleName, composableName) { ResolvedAttribute(moduleName, composableName) } + PythonWidget(resolved.value, modifier) +} + +/** + * [PythonWidget] for a Python object the host already holds. * * [root] is one of: * @@ -37,28 +102,32 @@ import python.native.ffi.PyLong_AsLongLong * [root] is the caller's: it is neither consumed nor closed. */ @Composable -fun PythonContent(root: PyObject) { +fun PythonWidget(root: PyObject, modifier: Modifier = Modifier) { val source = remember(root) { RootSource.of(root) } val current = source.current() - if (current != null) { - key(current) { - PythonRoot(current) + Layout( + content = { + if (current != null) { + key(current) { + PythonRoot(current) + } + } + }, + modifier = modifier, + ) { measurables, constraints -> + val placeables = measurables.map { it.measure(constraints) } + val width = placeables.maxOfOrNull { it.width } ?: constraints.minWidth + val height = placeables.maxOfOrNull { it.height } ?: constraints.minHeight + layout(width, height) { + placeables.forEach { it.place(0, 0) } } } } -/** - * [PythonContent] for a root state that lives at `module.attribute` -- `PythonContent("main", "app")` - * for a Python `main.app`. - * - * The attribute is resolved **once**, when this enters the composition (and again only if [module] - * or [attribute] change). Rebinding the module attribute afterwards is not observed; replacing the - * root is a write into the state the attribute names, which is. - */ +/** [PythonWidget] with a `root`, under the app-root name. */ @Composable -fun PythonContent(module: String, attribute: String) { - val resolved = remember(module, attribute) { ResolvedAttribute(module, attribute) } - PythonContent(resolved.value) +private fun PythonAppView(root: PyObject, modifier: Modifier) { + PythonWidget(root, modifier) } /** diff --git a/python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/RememberSaveable.kt b/python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/RememberSaveable.kt new file mode 100644 index 000000000..947a1cab2 --- /dev/null +++ b/python-multiplatform-compose/src/commonMain/kotlin/python/multiplatform/compose/RememberSaveable.kt @@ -0,0 +1,53 @@ +package python.multiplatform.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import python.multiplatform.ffi.PyObject + +/** + * `rememberSaveable { mutableStateOf(initial) }` for a value Python holds, so that a Python screen's + * state survives what `rememberSaveable` survives (recreating an activity, process death with a + * saved instance state, a `SaveableStateRegistry` saved and restored). + * + * Follows `rememberSaveableWrapper(init, type)` of the original pycomposeui + * (`Runtime.android.kt`), which dispatched on a type name Python passed along. Here the value alone + * is passed: a Python scalar written into a `kotlin.Any?` slot arrives boxed, so its runtime type is + * the type name. + * + * | Python value | Kotlin type | State created | + * |---|---|---| + * | `int` that fits 32 bits | `Int` | `mutableIntStateOf` | + * | larger `int` | `Long` | `mutableLongStateOf` | + * | `float` | `Double` | `mutableDoubleStateOf` | + * | `bool` | `Boolean` | `mutableStateOf` | + * | `str` | `String` | `mutableStateOf` | + * + * Any other value throws [IllegalArgumentException] naming its type (`list` for a Python list): like the original, only these + * are supported, because only these are saveable without a custom `Saver`. + * + * The returned state is Compose's own: Python reads and writes its `value`, and a write is saved by + * the next `SaveableStateRegistry.performSave`. Call it inside a composition only. + */ +@Composable +fun rememberSaveableWrapper(initial: Any?): MutableState<*> = when (initial) { + is Int -> rememberSaveable { mutableIntStateOf(initial) } + is Long -> rememberSaveable { mutableLongStateOf(initial) } + is Double -> rememberSaveable { mutableDoubleStateOf(initial) } + is Boolean -> rememberSaveable { mutableStateOf(initial) } + is String -> rememberSaveable { mutableStateOf(initial) } + else -> throw IllegalArgumentException( + "rememberSaveableWrapper supports Int, Long, Double, Boolean and String, not " + typeNameOf(initial), + ) +} + +/** The type of [value] as Python spells it for a Python object (`list`), as Kotlin does otherwise. */ +private fun typeNameOf(value: Any?): String = when (value) { + null -> "null" + is PyObject -> value.Type.getAttr("__name__").toString() + else -> value::class.qualifiedName ?: value::class.simpleName ?: "unknown" +} diff --git a/python-multiplatform-gradle-plugin/build.gradle.kts b/python-multiplatform-gradle-plugin/build.gradle.kts index 4f767181e..e19b5dd37 100644 --- a/python-multiplatform-gradle-plugin/build.gradle.kts +++ b/python-multiplatform-gradle-plugin/build.gradle.kts @@ -72,7 +72,7 @@ dependencies { // - not in the library. `python-multiplatform` does not depend on this plugin; // `:python-multiplatform:dependencies` reports `kotlin-compiler-embeddable:2.0.20` in no // configuration at all, `desktopRuntimeClasspath` included. - // - not on a consumer's *application* classpath. `:ksp-fixtures:app:dependencies` reports zero + // - not on a consumer's *application* classpath. `:python-multiplatform-ksp:fixtures:app:dependencies` reports zero // occurrences of `2.0.20`. (It does report `kotlin-compiler-embeddable:2.4.0` and // `2.4.20-Beta2` under `kotlinCompilerPluginClasspath*` and the commonizer's configurations // -- KGP's own, present before this dependency existed.) @@ -125,7 +125,7 @@ dependencies { // classloader's parent is Gradle's own minimal worker infrastructure, not that root scope, so // `org.jetbrains.kotlin.library.*` has exactly one source inside the worker. Confirmed against a // real consumer build, not only this plugin's own unit tests: - // `:ksp-fixtures:klib-artifact:generatePythonArtifactBindings` and `:generatePythonStubs` both + // `:python-multiplatform-gradle-plugin-fixtures:klib-artifact:generatePythonArtifactBindings` and `:generatePythonStubs` both // pass and emit the walker's real output (`kotlinx.coroutines.flow.internal.checkIndexOverflow`). // // ### Compile-only here, resolved by the consumer at klib-walking time @@ -243,8 +243,8 @@ val generateCoordinates = tasks.register("generateCoordinates") { val file = rootDir.resolve("../gradle.properties") if (file.isFile) file.inputStream().use { load(it) } } - val pythonVersionValue = rootProperties.getProperty("pythonVersion") ?: "3.14.7" - val pbsReleaseValue = rootProperties.getProperty("pythonBuildStandaloneRelease") ?: "20260807" + val pythonVersionValue = rootProperties.getProperty("pythonVersion") ?: "3.15.0" + val pbsReleaseValue = rootProperties.getProperty("pythonBuildStandaloneRelease") ?: "20261013" val freeThreadedValue = rootProperties.getProperty("pythonFreeThreaded")?.toBoolean() ?: false // `python-multiplatform` itself is versioned "$pythonVersion-alpha01" -- see that build's own diff --git a/ksp-fixtures/artifact-valueclass/build.gradle.kts b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/build.gradle.kts similarity index 90% rename from ksp-fixtures/artifact-valueclass/build.gradle.kts rename to python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/build.gradle.kts index 5c12a588a..7a0218307 100644 --- a/ksp-fixtures/artifact-valueclass/build.gradle.kts +++ b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/build.gradle.kts @@ -4,7 +4,7 @@ * * `kotlin.time.Duration` (the real-world case `ArtifactScanner`'s KDoc discusses) cannot prove a * *positive* round trip: its constructor is `internal`, so no generated Kotlin outside - * `kotlin-stdlib` can ever build one. This module exists only to be walked -- like `:ksp-fixtures + * `kotlin-stdlib` can ever build one. This module exists only to be walked -- like `:python-multiplatform-gradle-plugin-fixtures * :artifact`'s `junit` dependency, it is a plain `kotlin("jvm")` library with no bindings plugin of * its own applied, resolved as an ordinary dependency and never compiled by the module that walks * it. diff --git a/ksp-fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/Meters.kt b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/Meters.kt similarity index 82% rename from ksp-fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/Meters.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/Meters.kt index e627cab6a..92d67b377 100644 --- a/ksp-fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/Meters.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/Meters.kt @@ -1,4 +1,4 @@ -// Never compiled by `:ksp-fixtures:artifact`, which only ever sees this module's resolved jar -- +// Never compiled by `:python-multiplatform-gradle-plugin-fixtures:artifact`, which only ever sees this module's resolved jar -- // see the `build.gradle.kts` doc comment on why this module exists at all. package fixture.valueclass diff --git a/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/member/Members.kt b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/member/Members.kt new file mode 100644 index 000000000..259792250 --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/member/Members.kt @@ -0,0 +1,49 @@ +// Walked by `:python-multiplatform-gradle-plugin-fixtures:artifact` (its `fixture.valueclass` include covers this +// package): the member functions issue #215 binds, on a class and on an interface. +package fixture.valueclass.member + +/** A class with a constructor, overloaded members and a member that returns another handle. */ +class Tally(var total: Int) { + fun add(step: Int): Int { + total += step + return total + } + + fun add(step: Int, label: String): String = label + add(step) + + fun reset() { + total = 0 + } + + fun twin(): Tally = Tally(total) +} + +/** The `kotlinx.coroutines.Job` shape: only its members say anything about it. */ +interface Switch { + val on: Boolean + + fun turnOff() +} + +fun newSwitch(): Switch = object : Switch { + override var on: Boolean = true + + override fun turnOff() { + on = false + } +} + +/** Kotlin's factory idiom (`kotlinx.coroutines.Job` and `fun Job()`): an interface and a function of one name (issue #217). */ +interface Gauge { + val level: Int + + fun bump() +} + +fun Gauge(): Gauge = object : Gauge { + override var level: Int = 0 + + override fun bump() { + level += 1 + } +} diff --git a/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/sam/Sam.kt b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/sam/Sam.kt new file mode 100644 index 000000000..fbb20e980 --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/artifact-valueclass/src/main/kotlin/fixture/valueclass/sam/Sam.kt @@ -0,0 +1,22 @@ +// Walked by `:python-multiplatform-gradle-plugin-fixtures:artifact` (its `fixture.valueclass` include covers this +// package): functional-interface parameters issue #216 fills with a Python callable. +package fixture.valueclass.sam + +/** A Kotlin `fun interface` that is handed two values. */ +fun interface Listener { + fun onEvent(name: String, count: Int) +} + +/** The `CoroutineDispatcher.dispatch(context, block: Runnable)` shape, which runs the block at once. */ +fun runRunnable(block: Runnable) = block.run() + +/** Hands the listener a name and its length. */ +fun notifyListener(listener: Listener, name: String) = listener.onEvent(name, name.length) + +private var heard = "" + +/** A listener Kotlin made, so the same slot is shown taking a Kotlin handle as well as a callable. */ +fun loggingListener(): Listener = Listener { name, count -> heard = "$name:$count" } + +/** What [loggingListener]'s listener heard last. */ +fun lastHeard(): String = heard diff --git a/ksp-fixtures/artifact/build.gradle.kts b/python-multiplatform-gradle-plugin/fixtures/artifact/build.gradle.kts similarity index 93% rename from ksp-fixtures/artifact/build.gradle.kts rename to python-multiplatform-gradle-plugin/fixtures/artifact/build.gradle.kts index 0b37dedd8..f7369a9c5 100644 --- a/ksp-fixtures/artifact/build.gradle.kts +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/build.gradle.kts @@ -6,11 +6,11 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget * installed into the same `UpcallTable` KSP's own fragments go into, and imported from Python. * * `docs/design/ecosystem.md` §5b names two producers of bindings and splits them by *what they look at* -- - * KSP the consumer's own source, the walker everything the build resolves. `:ksp-fixtures:app` + * KSP the consumer's own source, the walker everything the build resolves. `:python-multiplatform-ksp:fixtures:app` * covers the first. Nothing covered the second, which is why `androidx.compose.material3` was * believed to be unreachable. * - * Deliberately a module of its own rather than another target in `:ksp-fixtures:app`: that module's + * Deliberately a module of its own rather than another target in `:python-multiplatform-ksp:fixtures:app`: that module's * `CommonInstallTest` asserts `UpcallTable.moduleNames` **exactly**, and a walked artefact appearing * there would be a change to what `FunctionTable` means rather than an addition beside it. Keeping * the two apart in the build is the same judgement `ArtifactTableRenderingTest` records for the @@ -46,7 +46,7 @@ kotlin { implementation(libs.junit) // See its own `build.gradle.kts`: a real jar for the value-class round trip // `kotlin.time.Duration` cannot prove. - implementation(projects.kspFixtures.artifactValueclass) + implementation(projects.pythonMultiplatformGradlePluginFixtures.artifactValueclass) // Compose Multiplatform, as plain Maven coordinates and **without** the Compose // Gradle or compiler plugin. That is deliberate and is the whole shape of the claim: @@ -99,7 +99,7 @@ pythonBindings { // `ArtifactScanner`'s KDoc names as unreachable before it read `@Metadata` -- a top-level // extension function behind a `MULTI_FILE_CLASS_FACADE`. `WalkedArtifactPythonImportTest` calls // it from real Python. - // `fixture.valueclass` is `:ksp-fixtures:artifact-valueclass`'s `Meters`/`sumMeters` -- see that + // `fixture.valueclass` is `:python-multiplatform-gradle-plugin-fixtures:artifact-valueclass`'s `Meters`/`sumMeters` -- see that // module's `build.gradle.kts` for why `kotlin.time.Duration` cannot stand in for it. // // `androidx.compose.foundation.layout` is the scorecard of @@ -127,6 +127,10 @@ pythonBindings { "androidx.compose.foundation.layout", "androidx.compose.foundation.ClickableKt", "kotlin.system.TimingKt", + // `TextUnit(30, TextUnitType.Sp)`: a value class over `Long` whose property is internal, + // so it can only ever be held in Python as a boxed handle (issue #168). + "androidx.compose.ui.unit.TextUnitKt", + "androidx.compose.ui.unit.TextUnitType", ), ) } diff --git a/ksp-fixtures/artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt similarity index 100% rename from ksp-fixtures/artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt diff --git a/ksp-fixtures/artifact/src/desktopMain/kotlin/fixture/artifact/ComposeSeed.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopMain/kotlin/fixture/artifact/ComposeSeed.kt similarity index 94% rename from ksp-fixtures/artifact/src/desktopMain/kotlin/fixture/artifact/ComposeSeed.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopMain/kotlin/fixture/artifact/ComposeSeed.kt index ef0651b0c..25a8f5873 100644 --- a/ksp-fixtures/artifact/src/desktopMain/kotlin/fixture/artifact/ComposeSeed.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopMain/kotlin/fixture/artifact/ComposeSeed.kt @@ -6,7 +6,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp /** * The declarations `WalkedArtifactComposeModifierTest` needs that the *walker* cannot supply, @@ -84,3 +86,8 @@ fun equalsVerticalPadding(modifier: Modifier, vertical: Double): Boolean = /** The negative control for [equalsVerticalPadding]: the same number in the *horizontal* slot. */ fun equalsHorizontalPadding(modifier: Modifier, horizontal: Double): Boolean = modifier == Modifier.padding(horizontal = horizontal.dp) + +/** Structural equality of a boxed `TextUnit` against `size.sp`; the box is what a handle resolves to. */ +fun equalsSp(value: TextUnit, size: Double): Boolean = value == size.sp + +fun describeValue(value: TextUnit): String = value.toString() diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/PythonxModules.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/PythonxModules.kt similarity index 100% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/PythonxModules.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/PythonxModules.kt diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/StubSignatureAgreesWithRuntimeTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/StubSignatureAgreesWithRuntimeTest.kt similarity index 95% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/StubSignatureAgreesWithRuntimeTest.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/StubSignatureAgreesWithRuntimeTest.kt index 2169d808d..87c11218d 100644 --- a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/StubSignatureAgreesWithRuntimeTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/StubSignatureAgreesWithRuntimeTest.kt @@ -91,10 +91,13 @@ class StubSignatureAgreesWithRuntimeTest { # a value class bound as its primitive: the raw number crosses (`Dp | float`, or `float` # where the class name is taken by a function in its module) return TAG[tag] in parts - # an object: the stub class named by its FQN (or bare inside its own module), or `Any` - # where a function in its module owns the name -- never an `int` handle + # an object: the stub class named by its FQN (or bare inside its own module), or -- where a + # function in its module owns the name -- its stub-private class `_Name` (issue #217), or + # `Any`; never an `int` handle simple = runtime.rsplit('.', 1)[-1] - return parts[0].endswith('Any') or runtime in stub or parts[0] == simple + private = '_' + simple + return (parts[0].endswith('Any') or runtime in stub or parts[0] == simple or + parts[0] == private or parts[0].endswith('.' + private)) def stub_parameters(fn): a = fn.args diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactCallbackTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactCallbackTest.kt similarity index 96% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactCallbackTest.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactCallbackTest.kt index f5c06d447..8c6447608 100644 --- a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactCallbackTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactCallbackTest.kt @@ -41,7 +41,7 @@ import kotlin.test.assertTrue * () -> Unit)` has the identical slot (`kotlin.Function0()->kotlin.Unit`, same walker, same * fragment, same `PythonFunction` wrapper) and invokes it at the moment it is called, so it is what * makes "the callback is called" a measurement rather than an inference. Rendering a real click is - * `:ksp-fixtures:compose`'s job and needs the Compose compiler plugin this module deliberately does + * `:python-multiplatform-compose:fixtures:compose`'s job and needs the Compose compiler plugin this module deliberately does * not apply. * * ### Lifetime @@ -87,13 +87,13 @@ class WalkedArtifactCallbackTest { PythonCallables.withScope(PythonCallables.newScope()) { Python3.exec( """ - from kotlin.system import measureTimeMillis + from kotlin.system import measure_time_millis _ran = [] _never = [] _control = lambda: _never.append('should not run') - _elapsed = measureTimeMillis(lambda: _ran.append('ran')) + _elapsed = measure_time_millis(lambda: _ran.append('ran')) assert _ran == ['ran'], 'the Python callable was not invoked: ' + repr(_ran) assert _never == [], 'a callable nobody passed anywhere ran: ' + repr(_never) @@ -120,9 +120,9 @@ class WalkedArtifactCallbackTest { PythonCallables.withScope(PythonCallables.newScope()) { Python3.exec( """ - from kotlin.system import measureTimeMillis + from kotlin.system import measure_time_millis try: - measureTimeMillis(lambda wanted: None) + measure_time_millis(lambda wanted: None) raise AssertionError('a one-argument callable filled a zero-argument slot') except AssertionError: raise @@ -166,7 +166,7 @@ class WalkedArtifactCallbackTest { assert modifierElementCount(_empty) == 0 _clicks = [] - _tappable = clickable(_empty, onClick=lambda: _clicks.append('tap')) + _tappable = clickable(_empty, on_click=lambda: _clicks.append('tap')) assert not isTheEmptyModifier(_tappable._pm_handle), ( 'clickable returned the receiver unchanged' diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactComposeModifierTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactComposeModifierTest.kt similarity index 100% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactComposeModifierTest.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactComposeModifierTest.kt diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactDefaultOmissionTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactDefaultOmissionTest.kt similarity index 95% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactDefaultOmissionTest.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactDefaultOmissionTest.kt index 25adbc2cd..16abfcf44 100644 --- a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactDefaultOmissionTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactDefaultOmissionTest.kt @@ -73,7 +73,8 @@ class WalkedArtifactDefaultOmissionTest { /** * The judgement, in Compose's own code: two calls that write **no** optional argument. * - * `fillMaxWidth(m)` and `wrapContentSize(m)` are called with the receiver alone -- + * `fill_max_width(m)` and `wrapContentSize(m)` -- the first by its Pythonic name (issue #131), the + * second by its Kotlin name -- are called with the receiver alone -- * `f(x)`, not `f(x, None, None)` -- and the receiver is the only required parameter either of * them declares. */ @@ -81,14 +82,14 @@ class WalkedArtifactDefaultOmissionTest { fun composeFunctionsAreCalledWithTheirRequiredArgumentsAlone() { Python3.exec( """ - from androidx.compose.foundation.layout import fillMaxWidth, wrapContentSize + from androidx.compose.foundation.layout import fill_max_width, wrapContentSize from fixture.artifact import emptyModifier from fixture.artifact import equalsFillMaxWidth, equalsFillMaxWidthFraction from fixture.artifact import equalsWrapContentSize, equalsWrapContentSizeTopStart - _filled = fillMaxWidth(emptyModifier()) + _filled = fill_max_width(emptyModifier()) assert equalsFillMaxWidth(_filled._pm_handle), ( - 'fillMaxWidth() did not produce Modifier.fillMaxWidth()' + 'fill_max_width() did not produce Modifier.fillMaxWidth()' ) # The controls. `fraction` defaults to 1f, so a body that had passed the sentinel through # as a number would have produced one of these instead. diff --git a/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactMemberFunctionTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactMemberFunctionTest.kt new file mode 100644 index 000000000..fdee2f946 --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactMemberFunctionTest.kt @@ -0,0 +1,84 @@ +package fixture.artifact + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.pythonx.PythonxAdapter +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.ffi.upcall.UpcallBootstrap +import python.multiplatform.generated.FunctionTable +import python.multiplatform.generated.artifacts.ArtifactTable +import python.multiplatform.reflection.UpcallTable +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +/** + * Issue #215, end to end: member functions of a walked class and interface + * (`fixture.valueclass.member`, from the `artifact-valueclass` jar) are called from Python on the + * handles Kotlin returned, through the generated table and the binding layer. + * + * Red before #215: `tally.add(3)` and `switch.turn_off()` are `AttributeError`s, because the walker + * bound only the constructor and the properties. + */ +class WalkedArtifactMemberFunctionTest { + + @BeforeTest + fun installBothProducers() { + Python3.initialize(silent = true) + UpcallTable.clear() + UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments) + check(UpcallBootstrap.publishToGlobals()) { "UpcallBootstrap.publishToGlobals() failed" } + PythonProxySource.install() + PythonxAdapter.install(PYTHONX_RAW_VALUE_CLASSES) + } + + @AfterTest + fun cleanup() { + UpcallTable.clear() + } + + @Test + fun aClassesMembersRunOnTheKotlinObject() { + Python3.exec( + """ + from fixture.valueclass.member import Tally + _mf_t = Tally(1) + assert _mf_t.add(3) == 4, 'add(Int) did not run on the Kotlin object' + assert _mf_t.add(1, 'n=') == 'n=5', 'add(Int, String) was not chosen by its arguments' + assert _mf_t.add__Int(10) == 15, 'the explicit overload name did not reach add(Int)' + assert _mf_t.total == 15, 'the members did not change the object the proxy holds' + _mf_twin = _mf_t.twin() + _mf_t.reset() + assert _mf_t.total == 0, 'reset() did not run' + assert _mf_twin.total == 15, "a member's handle result is not a separate Kotlin object" + assert type(_mf_twin)._kotlin_type_name == 'fixture.valueclass.member.Tally', type(_mf_twin)._kotlin_type_name + """.trimIndent(), + ) + } + + @Test + fun anInterfacesMemberRunsOnTheObjectBehindIt() { + Python3.exec( + """ + from fixture.valueclass.member import newSwitch + _mf_s = newSwitch() + assert _mf_s.on is True + _mf_s.turn_off() + assert _mf_s.on is False, 'turn_off() did not run on the object behind the interface' + """.trimIndent(), + ) + } + + /** Issue #217: at run time the factory owns the name, and the value it makes keeps its members. */ + @Test + fun aValueOfATypeWhoseNameAFactoryTakesKeepsItsMembers() { + Python3.exec( + """ + from fixture.valueclass.member import Gauge + _mf_g = Gauge() + assert _mf_g.level == 0, repr(_mf_g.level) + _mf_g.bump() + assert _mf_g.level == 1, 'the member did not run on the value the factory made' + """.trimIndent(), + ) + } +} diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactPythonImportTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactPythonImportTest.kt similarity index 98% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactPythonImportTest.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactPythonImportTest.kt index ae8d02744..e7bc5d65d 100644 --- a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactPythonImportTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactPythonImportTest.kt @@ -137,7 +137,7 @@ class WalkedArtifactPythonImportTest { /** * `fixture.valueclass.sumMeters`: both parameters and the return are a public - * `@JvmInline value class` (`Meters`), from `:ksp-fixtures:artifact-valueclass` -- a jar built + * `@JvmInline value class` (`Meters`), from `:python-multiplatform-gradle-plugin-fixtures:artifact-valueclass` -- a jar built * for exactly this proof, since `kotlin.time.Duration` (the real-world case) has an `internal` * constructor and so can never round-trip. `12.0` is not producible except by unwrapping two * `Meters` arguments, adding them, and wrapping the sum back up. diff --git a/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactSamParameterTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactSamParameterTest.kt new file mode 100644 index 000000000..5265764cc --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactSamParameterTest.kt @@ -0,0 +1,76 @@ +package fixture.artifact + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.pythonx.PythonCallables +import python.multiplatform.ffi.pythonx.PythonxAdapter +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.ffi.upcall.UpcallBootstrap +import python.multiplatform.generated.FunctionTable +import python.multiplatform.generated.artifacts.ArtifactTable +import python.multiplatform.reflection.UpcallTable +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +/** + * Issue #216, end to end: a Python callable fills a `java.lang.Runnable` and a Kotlin `fun interface` + * parameter of a walked declaration (`fixture.valueclass.sam`), and Kotlin runs it. + * + * Red before #216: `run_runnable` and `notify_listener` do not exist, because the walker declined both. + */ +class WalkedArtifactSamParameterTest { + + @BeforeTest + fun installBothProducers() { + Python3.initialize(silent = true) + UpcallTable.clear() + UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments) + check(UpcallBootstrap.publishToGlobals()) { "UpcallBootstrap.publishToGlobals() failed" } + PythonProxySource.install() + PythonxAdapter.install(PYTHONX_RAW_VALUE_CLASSES) + } + + @AfterTest + fun cleanup() { + UpcallTable.clear() + } + + @Test + fun aPythonCallableRunsAsARunnable() { + PythonCallables.withScope(PythonCallables.newScope()) { + Python3.exec( + """ + from fixture.valueclass.sam import run_runnable + _sam_ran = [] + run_runnable(lambda: _sam_ran.append('ran')) + assert _sam_ran == ['ran'], 'the Runnable did not run the Python callable: ' + repr(_sam_ran) + """.trimIndent(), + ) + } + } + + @Test + fun aPythonCallableRunsAsAFunInterfaceWithItsArguments() { + PythonCallables.withScope(PythonCallables.newScope()) { + Python3.exec( + """ + from fixture.valueclass.sam import notify_listener + _sam_events = [] + notify_listener(lambda name, count: _sam_events.append((name, count)), 'hello') + assert _sam_events == [('hello', 5)], repr(_sam_events) + """.trimIndent(), + ) + } + } + + @Test + fun aKotlinImplementationStillFillsTheSameSlot() { + Python3.exec( + """ + from fixture.valueclass.sam import notify_listener, logging_listener, last_heard + notify_listener(logging_listener(), 'abc') + assert last_heard() == 'abc:3', 'a Kotlin Listener handle did not reach the slot: ' + repr(last_heard()) + """.trimIndent(), + ) + } +} diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactStubTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactStubTest.kt similarity index 76% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactStubTest.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactStubTest.kt index 4ed7735b2..ee91ab5e8 100644 --- a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactStubTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactStubTest.kt @@ -7,6 +7,7 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue /** @@ -86,16 +87,17 @@ class WalkedArtifactStubTest { /** * The binder never exports a Kotlin namespace under another name (AGENTS.md section 12 rule 1). - * `androidx.compose.foundation.layout` is stubbed under that very path, with Kotlin names and - * Kotlin parameter names; making it Pythonic is the `pythonx-compose` package's job. + * `androidx.compose.foundation.layout` is stubbed under that very path. Inside it (issue #131) + * each declaration keeps its Kotlin `def` and gains its Pythonic alias as an assignment, and a + * parameter is written under the keyword `inspect.signature` shows. * - * Pinned over the whole generated tree: nothing under `pythonx/`, no snake_cased member, and none - * of the Pythonic product's side files (`py.typed`, `_pm_dispatch.json`). The `Protocol` classes - * that type an extension as a method of its receiver's class are typing machinery, not a - * renamed product: they are private (`_Receiver_name`) and carry the Kotlin name unchanged. + * Pinned over the whole generated tree: nothing under `pythonx/`, and none of the old Pythonic + * product's side files (`py.typed`, `_pm_dispatch.json`). The `Protocol` classes that type an + * extension as a method of its receiver's class are typing machinery: they are private + * (`_Receiver_name`) and named after the Kotlin member. */ @Test - fun theStubsUseKotlinNamesAndNothingIsExportedUnderPythonx() { + fun theStubsKeepKotlinNamesAddPythonicAliasesAndExportNothingUnderPythonx() { val paths = generatedPaths() assertEquals(emptyList(), paths.filter { it.startsWith("pythonx/") }, paths.toString()) assertTrue(paths.none { it.endsWith("py.typed") || it.endsWith("_pm_dispatch.json") }, paths.toString()) @@ -106,16 +108,25 @@ class WalkedArtifactStubTest { "-> androidx.compose.ui.Modifier:" in layout, layout.take(3000), ) - assertTrue("fill_max_width" !in layout, "a Kotlin name was exported under another spelling") - assertTrue("alignmentLine: " in layout, "a Kotlin parameter name was renamed: ${layout.take(3000)}") + assertTrue( + Regex("^fill_max_width = fillMaxWidth${'$'}", RegexOption.MULTILINE).containsMatchIn(layout), + "the Pythonic alias of fillMaxWidth is missing", + ) + assertTrue(Regex("^def fill_max_width\\(", RegexOption.MULTILINE).find(layout) == null, "the Kotlin def must stay the Kotlin name") + assertTrue("alignment_line: " in layout, "a parameter is not under its Pythonic keyword: ${layout.take(3000)}") paths.filter { it.endsWith(".pyi") }.forEach { path -> val text = stub(path) assertTrue("pythonx" !in text, "$path mentions pythonx") - assertTrue( - Regex("""class _(?!\w+_\w+\(_t\.Protocol\))""").find(text) == null, - "$path carries a private class that is not a receiver-method protocol", - ) + // A private class is a receiver-method protocol, or (issue #217) the stub-private class of a type + // whose name a function of its module takes, which says so in its docstring. + Regex("""class (_\w+)[^\n]*:\n([^\n]*)""").findAll(text).forEach { match -> + if (Regex("""^class _\w+_\w+\(_t\.Protocol\)""").containsMatchIn(match.value.trimStart())) return@forEach + assertTrue( + "(stubbed as ${match.groupValues[1]}:" in match.groupValues[2], + "$path carries a private class that is neither a receiver-method protocol nor a taken name's class: ${match.value}", + ) + } } } @@ -224,11 +235,39 @@ class WalkedArtifactStubTest { .toSet() assertEquals(emptySet(), constructors - installed, "a constructor stub names no table key") + // A fifth shape (issue #215): a member function is a method of its receiver's stub class -- or, + // where the class has no stub, a comment saying so -- and either way it carries its table key + // after `Kotlin member: `. Matched by key, like a property. + val members = generatedPaths() + .filter { it.endsWith(".pyi") } + .flatMap { path -> + Regex("""Kotlin member: ([^\s"]+)""").findAll(stub(path)).map { it.groupValues[1] }.toList() + } + .toSet() + val memberKeys = UpcallTable.entries().filter { it.kind.name == "METHOD" }.map { it.name }.toSet() + assertTrue("fixture.valueclass.member.Tally.reset" in memberKeys, "no member function was bound: $memberKeys") + assertEquals(emptySet(), members - memberKeys, "a member stub names no member key") + assertEquals(emptySet(), memberKeys - members, "a member key has no stub") + // The base name of an overload set (`padding` for `padding__Dp`, ...) is served by the binding // layer's dispatcher and is stubbed as `@overload`s; it is not itself a table key. val overloadBases = installed.map { it.substringBeforeLast('.') + "." + it.substringAfterLast('.').substringBefore("__") } .filter { it !in installed }.toSet() assertEquals(emptySet(), stubbed - installed - overloadBases, "stubbed but not callable") - assertEquals(emptySet(), installed - propertyKeys - stubbed - constructors, "callable but not stubbed") + assertEquals(emptySet(), installed - propertyKeys - memberKeys - stubbed - constructors, "callable but not stubbed") + } + + /** + * Issue #217: `fixture.valueclass.member.Gauge` is an interface and a function. The stub's `Gauge` is + * the function, as at run time, and the type is `_Gauge`, with its property and its member. + */ + @Test + fun aTypeWhoseNameAFactoryTakesIsStubbedUnderAPrivateName() { + val stub = stub("fixture/valueclass/member/__init__.pyi") + assertTrue("def Gauge() -> _Gauge:" in stub, stub) + assertTrue("class _Gauge:" in stub, stub) + assertTrue(" def level(self) -> int:" in stub, stub) + assertTrue(" def bump(self) -> None:" in stub, stub) + assertFalse("is not stubbed" in stub, stub) } } diff --git a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactTableTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactTableTest.kt similarity index 81% rename from ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactTableTest.kt rename to python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactTableTest.kt index 569fa47bf..586578061 100644 --- a/ksp-fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactTableTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactTableTest.kt @@ -32,7 +32,7 @@ class WalkedArtifactTableTest { /** * The wiring, pinned against the two artefacts this fixture fully controls. * - * `junit:junit:4.13.2` and `:ksp-fixtures:artifact-valueclass` are both version-pinned by this + * `junit:junit:4.13.2` and `:python-multiplatform-gradle-plugin-fixtures:artifact-valueclass` are both version-pinned by this * build, so their contribution is a fixed list and `ArtifactScannerTest * .theWholeJUnitJarYieldsExactlyTheseUnsuffixedDeclarations` pins the same seven names from the * other end -- that test walks the jar directly in a plugin unit test, this one reads what the @@ -53,12 +53,34 @@ class WalkedArtifactTableTest { assertEquals( listOf( - // `:ksp-fixtures:artifact-valueclass`, built for exactly one proof: see that + // `:python-multiplatform-gradle-plugin-fixtures:artifact-valueclass`, built for exactly one proof: see that // module's `build.gradle.kts`. `Meters` is the value class's own constructor, // bound since `e2d75100` -- this list predates that and was never updated, which - // nothing noticed because `ksp-fixtures:artifact` is not in the usual verification + // nothing noticed because `python-multiplatform-gradle-plugin-fixtures:artifact` is not in the usual verification // set. `ArtifactScannerTest` pins the same constructor from the plugin side. "fixture.valueclass.Meters", + // `fixture.valueclass.member` (issue #215): member functions as `METHOD` entries, beside + // the class's constructor and its properties. `WalkedArtifactMemberFunctionTest` calls them. + "fixture.valueclass.member.Gauge", + "fixture.valueclass.member.Gauge.bump", + "fixture.valueclass.member.Gauge.level", + "fixture.valueclass.member.Switch.on", + "fixture.valueclass.member.Switch.turnOff", + "fixture.valueclass.member.Tally", + "fixture.valueclass.member.Tally.add__Int", + "fixture.valueclass.member.Tally.add__Int_String", + "fixture.valueclass.member.Tally.reset", + "fixture.valueclass.member.Tally.total", + "fixture.valueclass.member.Tally.total=", + "fixture.valueclass.member.Tally.twin", + "fixture.valueclass.member.newSwitch", + // `fixture.valueclass.sam` (issue #216): functional-interface parameters, filled with a Python + // callable. `WalkedArtifactSamParameterTest` calls them. + "fixture.valueclass.sam.Listener.onEvent", + "fixture.valueclass.sam.lastHeard", + "fixture.valueclass.sam.loggingListener", + "fixture.valueclass.sam.notifyListener", + "fixture.valueclass.sam.runRunnable", "fixture.valueclass.sumMeters", "junit.runner.BaseTestRunner.getFilteredTrace", "junit.runner.BaseTestRunner.getPreference__String", diff --git a/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactValueClassBoxTest.kt b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactValueClassBoxTest.kt new file mode 100644 index 000000000..f2c9ae87e --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/artifact/src/desktopTest/kotlin/fixture/artifact/WalkedArtifactValueClassBoxTest.kt @@ -0,0 +1,71 @@ +package fixture.artifact + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.ffi.upcall.UpcallBootstrap +import python.multiplatform.generated.FunctionTable +import python.multiplatform.generated.artifacts.ArtifactTable +import python.multiplatform.reflection.UpcallTable +import python.multiplatform.ffi.pythonx.PythonxAdapter +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +/** + * Issue #168: a value class parameter takes the boxed value of its own class, which is what Kotlin + * itself would pass, and still refuses a raw number unless the type is on the raw-primitive allowlist. + * + * `TextUnitType` is a value class over `Long` with an internal property, so `TextUnitType.Sp` reaches + * Python only as a boxed handle. `TextUnit(30, TextUnitType.Sp)` used to fail with + * `expected a number for TextUnitType`. + */ +class WalkedArtifactValueClassBoxTest { + + @BeforeTest + fun install() { + Python3.initialize(silent = true) + UpcallTable.clear() + UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments) + check(UpcallBootstrap.publishToGlobals()) { "UpcallBootstrap.publishToGlobals() failed" } + PythonProxySource.install() + PythonxAdapter.install(PYTHONX_RAW_VALUE_CLASSES) + } + + @AfterTest + fun cleanup() { + UpcallTable.clear() + } + + @Test + fun aBoxedValueClassFillsItsOwnParameter() { + Python3.exec( + """ + from androidx.compose.ui.unit import TextUnit, TextUnitType + from fixture.artifact import equalsSp, describeValue + + _sp = TextUnit(30, TextUnitType.Sp) + assert equalsSp(_sp._pm_handle, 30.0), 'TextUnit(30, TextUnitType.Sp) is not 30.sp: ' + describeValue(_sp._pm_handle) + ' / ' + describeValue(TextUnitType.Sp._pm_handle) + assert not equalsSp(_sp._pm_handle, 31.0), '30.sp matched 31.sp' + """.trimIndent(), + ) + } + + @Test + fun aRawNumberStillDoesNotFillAValueClassOffTheAllowlist() { + Python3.exec( + """ + from androidx.compose.ui.unit import TextUnit + + # The slot is an object slot now, so a bare number is read as a handle and Kotlin refuses it + # (no live handle, or a handle that is not a TextUnitType): it is never reinterpreted as + # the packed Long, which would be a TextUnitType of the wrong unit and no error. + try: + _result = TextUnit(30, 4294967296) + except Exception: + pass + else: + raise AssertionError('a raw number was accepted for TextUnitType: ' + repr(_result)) + """.trimIndent(), + ) + } +} diff --git a/python-multiplatform-gradle-plugin/fixtures/klib-artifact/build.gradle.kts b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/build.gradle.kts new file mode 100644 index 000000000..7e8a8f00a --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/build.gradle.kts @@ -0,0 +1,121 @@ +/** + * The artefact walker's klib half, end to end: a real third-party `.klib` the build resolves, + * walked at build time by `KlibScanner`, installed into the same `UpcallTable` KSP's own fragments + * go into. + * + * `:python-multiplatform-gradle-plugin-fixtures:artifact` is `docs/design/ecosystem.md` §5b's second producer over a **jar** -- this is + * the same producer over a **klib**. Kotlin/Native targets rather than `desktop`: a jar is what a JVM + * target resolves, a klib is what a Kotlin/Native target resolves, and `PythonArtifactBindingsTask + * .artifactConfiguration`/`.artifactSourceSet` are single-valued (ROADMAP §16f #1) -- one module can + * wire one walked configuration, so this fixture is the klib producer's own module rather than a + * second target bolted onto `:artifact`. Since issue #234 it builds `androidNativeArm64` and + * `iosSimulatorArm64`: the androidNativeArm64 klib is walked, the table is generated into the shared + * `nativeMain`, and the suite in `nativeTest` runs through Gradle on the simulator. + * + * `kotlinx-coroutines-core` is the klib walked, for the same reason JUnit is `:artifact`'s: a real, + * independently-versioned third-party artefact nothing here controls or cooperates with. + * + * ### On androidNativeArm64 the suite is linked by the build and run by hand (`iosSimulatorArm64Test` runs it) + * + * KGP gives `androidNativeArm64` a `linkDebugTestAndroidNativeArm64` and **no run task** -- it + * registers one only where it knows how to reach a host, and an Android device is neither the build + * machine nor `simctl`. `:python-multiplatform` solved that for itself with a hand-written + * `androidNativeArm64Test` task (device selection, `adb push --sync` of a 60 MB stdlib, a + * TeamCity-to-JUnit-XML parser) that is project-local. Until that is generalised, this module's + * suite runs like this, reusing the payload that task already stages on the device: + * + * ./gradlew :python-multiplatform:androidNativeArm64Test # once: stages lib/ and the stdlib + * ./gradlew :python-multiplatform-gradle-plugin-fixtures:klib-artifact:linkDebugTestAndroidNativeArm64 + * D=/data/local/tmp/pmp-nativetest-arm64-v8a + * adb push python-multiplatform-gradle-plugin/fixtures/klib-artifact/build/bin/androidNativeArm64/debugTest/test.kexe \ + * $D/klibtest.kexe + * adb shell "chmod 755 $D/klibtest.kexe && cd $D && \ + * LD_LIBRARY_PATH=$D/lib PYTHONHOME=$D ./klibtest.kexe --ktest_logger=TEAMCITY" + * + * **A green ordinary build therefore says these tests compile and link, not that they pass.** + */ +plugins { + alias(libs.plugins.kotlin.multiplatform) + id("io.github.thisisthepy.python.multiplatform.bindings") +} + +val pythonDownloadDir = project(":python-multiplatform").layout.buildDirectory.dir("python-standalone").get().asFile +val fixturePythonVersion = project.findProperty("pythonVersion")?.toString() ?: project.rootProject.version.toString() +val fixturePythonLibVersion = fixturePythonVersion.split('.').subList(0, 2).joinToString(".") + +kotlin { + // Applied now rather than after evaluation, so `nativeMain`/`nativeTest` exist for the blocks below + // and for `pythonBindings.artifactSourceSet` (issue #234). + applyDefaultHierarchyTemplate() + + androidNativeArm64 { + // Mirrors `:python-multiplatform-ksp:fixtures:app`'s own androidNativeArm64 linker config: the test binary needs + // the same CPython symbols resolved at final link time, since `python-multiplatform`'s klib + // only declares them via cinterop and does not itself embed a static libpython. + val targetExtractDir = "$pythonDownloadDir/extracted/$fixturePythonVersion/android-aarch64/prefix" + binaries.getTest(org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG).linkerOpts.addAll( + listOf("-L$targetExtractDir/lib/", "-lpython$fixturePythonLibVersion", "-Wl,--allow-shlib-undefined"), + ) + } + // Issue #234: the simulator target, so the suite runs through Gradle (`iosSimulatorArm64Test`) + // rather than only being linked. Linked the way `:python-multiplatform`'s own simulator test + // binary is: against the xcframework's simulator slice, with an explicit `-rpath`, because + // `Python.framework` has an @rpath-relative install name and is not copied beside the binary. + iosSimulatorArm64 { + val slice = "$pythonDownloadDir/extracted/$fixturePythonVersion/ios/Python.xcframework/ios-arm64_x86_64-simulator" + binaries.getTest(org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG).linkerOpts.addAll( + listOf("-framework", "Python", "-F$slice", "-rpath", slice), + ) + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.pythonMultiplatform) + } + } + // Both native targets share the walked table and the suite (issue #234): the table is + // generated into `nativeMain`, and the tests live in `nativeTest`. + val nativeMain by getting { + dependencies { + // A real, separately-published klib. Nothing about it knows it is going to be + // walked -- KlibScanner reads its ABI the same way it would read anyone else's. + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") + } + } + val nativeTest by getting { + dependencies { + implementation(libs.kotlin.test) + } + } + } +} + +// The iOS `Python.framework` carries no standard library; `:python-multiplatform` unpacks the +// simulator's, and the test binary is pointed at it the way that module points its own. +tasks.withType().configureEach { + dependsOn(":python-multiplatform:extractIosSimulatorStdlib") + val pythonHome = project(":python-multiplatform").layout.buildDirectory.dir("python-stdlib/ios-simulator").get().asFile.absolutePath + // simctl only forwards variables into the spawned process when they carry this prefix. + environment("SIMCTL_CHILD_PYTHONHOME", pythonHome) + environment("PYTHONHOME", pythonHome) +} + +pythonBindings { + role.set("app") + processor.set(projects.pythonMultiplatformKsp) + + // The androidNativeArm64 klib is walked (what `KlibScannerTest` walks too); the table lands in + // `nativeMain`, so the simulator target compiles the same calls against its own klib. + artifactConfiguration.set("androidNativeArm64CompileKlibraries") + artifactSourceSet.set("nativeMain") + // Broad on purpose, unlike `:artifact`'s narrow `junit.runner`/`junit.framework`: coroutines' + // public surface is almost entirely `CoroutineScope`/`Job`/`Flow`-typed extensions, plus a couple + // of `@PublishedApi internal` declarations that are binary-visible but not source-visible outside + // the library that owns them. Those stay declined; since issue #234 the factories whose types are + // the klib's own classes bind (`Job`, `Mutex`, ...), and `WalkedKlibArtifactTableTest` pins the list. + // An earlier version bound the `@PublishedApi internal` one and failed to compile the first time + // its output was actually built rather than only unit-tested -- `KlibScanWorkAction`'s KDoc has + // the story. See that file's own KDoc for what the walker declines and why. + artifactIncludePackages.set(listOf("kotlinx.coroutines")) +} diff --git a/ksp-fixtures/klib-artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt similarity index 88% rename from ksp-fixtures/klib-artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt rename to python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt index 363edd21e..276ce3031 100644 --- a/ksp-fixtures/klib-artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt +++ b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/commonMain/kotlin/fixture/artifact/OwnDeclarations.kt @@ -7,7 +7,7 @@ package fixture.artifact * `ArtifactTable`. * * **Kept, not deleted with the rest of the copied-from-`:artifact` fixture.** Everything else this - * module inherited from `:ksp-fixtures:artifact` was a *jar*-path duplicate and went, but this file + * module inherited from `:python-multiplatform-gradle-plugin-fixtures:artifact` was a *jar*-path duplicate and went, but this file * is not about jars: `pythonBindings.role.set("app")` makes this module the aggregator, and an * aggregator with no source declarations of its own emits an **empty** `FunctionTable`. That * compiles and passes, and it quietly stops the fixture from demonstrating the one property diff --git a/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactHandleTest.kt b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactHandleTest.kt new file mode 100644 index 000000000..4d5b9271e --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactHandleTest.kt @@ -0,0 +1,84 @@ +package fixture.artifact + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.generated.FunctionTable +import python.multiplatform.generated.artifacts.ArtifactTable +import python.multiplatform.reflection.HandleTable +import python.multiplatform.reflection.ObjectReference +import python.multiplatform.reflection.UpcallTable +import python.native.ffi.UpcallEntry +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +/** + * Issue #234 on Kotlin/Native: a Kotlin object a walked klib declaration returns crosses as a handle, + * goes back into Kotlin as that object, and follows the jar side's lifetime rules -- the common + * trampoline's and `HandleTable`'s, which the klib boundary uses unchanged. + * + * - A handle Python holds is one `HandleTable` slot; passing it back into Kotlin (`Job(parent)`) + * neither releases it nor takes a second slot. + * - Dropping every Python reference releases each slot exactly once: the live count returns to its + * baseline, the raw handle no longer resolves, and a second release is refused (no double release). + * + * Red before #234: `Job` is not importable, because the walker declined every `kotlinx.coroutines` + * declaration. + */ +class WalkedKlibArtifactHandleTest { + + @BeforeTest + fun installTheWalkersTableIntoAnInterpreter() { + Python3.initialize(silent = true) + UpcallTable.clear() + UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments) + val mainGlobals = Python3.import("__main__").getAttr("__dict__") + check(UpcallEntry.publish(mainGlobals.pointer)) { "the upcall bootstrap could not be published" } + PythonProxySource.install() + } + + @AfterTest + fun cleanup() { + UpcallTable.clear() + } + + @Test + fun aHandleGoesBackIntoKotlinAndIsReleasedExactlyOnce() { + Python3.exec("import gc\ngc.collect()") + val baseline = HandleTable.liveCount + Python3.exec( + """ + from kotlinx.coroutines import Job + _kh_parent = Job(None) + _kh_raw = _kh_parent._pm_handle + _kh_children = [Job(_kh_parent) for _ in range(3)] + assert _kh_parent._pm_handle == _kh_raw, 'passing the handle into Kotlin changed it' + """.trimIndent(), + ) + // One slot for the parent and one per child: passing the parent in took no slot of its own. + assertEquals(baseline + 4, HandleTable.liveCount) + val raw = Python3.import("__main__").getAttr("_kh_raw").toString().toLong() + assertNotNull(HandleTable.resolveRaw(raw), "the parent's handle should still resolve while Python holds it") + + Python3.exec("del _kh_parent, _kh_children\nimport gc\ngc.collect()") + assertEquals(baseline, HandleTable.liveCount, "every slot is released exactly once") + assertNull(HandleTable.resolveRaw(raw), "a released handle must not resolve") + assertFalse(HandleTable.release(ObjectReference(raw)), "a second release must be refused") + } + + @Test + fun aNullableHandleParameterTakesNone() { + Python3.exec( + """ + from kotlinx.coroutines import Job + _kh_job = Job(None) + assert _kh_job is not None + del _kh_job + """.trimIndent(), + ) + } +} diff --git a/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactMemberFunctionTest.kt b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactMemberFunctionTest.kt new file mode 100644 index 000000000..b3c1ee771 --- /dev/null +++ b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactMemberFunctionTest.kt @@ -0,0 +1,73 @@ +package fixture.artifact + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.pythonx.PythonxAdapter +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.generated.FunctionTable +import python.multiplatform.generated.artifacts.ArtifactTable +import python.multiplatform.reflection.UpcallTable +import python.native.ffi.UpcallEntry +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +/** + * Issue #237 on Kotlin/Native: member functions of the walked klib's interfaces, called from Python on + * the handles its factories return, through the generated table and the binding layer -- #215's path, + * from a klib. + * + * - `Job(None)` is a `CompletableJob`: `complete()` is `True` the first time and `False` after, and + * `start()`, declared on its supertype `Job`, is reached through the ancestry the table carries. + * - A `Semaphore` of two permits gives two and refuses a third until one is released. + * + * Red before #237: `complete` and `try_acquire` are `AttributeError`s, because no member was bound. + */ +class WalkedKlibArtifactMemberFunctionTest { + + @BeforeTest + fun installBothLayers() { + Python3.initialize(silent = true) + UpcallTable.clear() + UpcallTable.install(FunctionTable.fragments + ArtifactTable.fragments) + val mainGlobals = Python3.import("__main__").getAttr("__dict__") + check(UpcallEntry.publish(mainGlobals.pointer)) { "the upcall bootstrap could not be published" } + PythonProxySource.install() + PythonxAdapter.install(emptySet()) + } + + @AfterTest + fun cleanup() { + UpcallTable.clear() + } + + @Test + fun aJobsMembersRunOnTheKotlinJob() { + Python3.exec( + """ + from kotlinx.coroutines import Job + _km_job = Job(None) + assert _km_job.complete() is True, 'complete() did not complete the job' + assert _km_job.complete() is False, 'a completed job completed twice' + _km_lazy = Job(None) + assert _km_lazy.start() is False, 'Job() is already active, so start() is False' + del _km_job, _km_lazy + """.trimIndent(), + ) + } + + @Test + fun aSemaphoresPermitsAreCountedByKotlin() { + Python3.exec( + """ + from kotlinx.coroutines.sync import Semaphore + _km_s = Semaphore(2, 0) + assert _km_s.try_acquire() is True + assert _km_s.try_acquire() is True + assert _km_s.try_acquire() is False, 'a third permit out of two' + _km_s.release() + assert _km_s.try_acquire() is True, 'a released permit was not given back' + del _km_s + """.trimIndent(), + ) + } +} diff --git a/ksp-fixtures/klib-artifact/src/androidNativeArm64Test/kotlin/fixture/artifact/WalkedKlibArtifactPythonImportTest.kt b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactPythonImportTest.kt similarity index 97% rename from ksp-fixtures/klib-artifact/src/androidNativeArm64Test/kotlin/fixture/artifact/WalkedKlibArtifactPythonImportTest.kt rename to python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactPythonImportTest.kt index 388e20a0f..a9e748d4c 100644 --- a/ksp-fixtures/klib-artifact/src/androidNativeArm64Test/kotlin/fixture/artifact/WalkedKlibArtifactPythonImportTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactPythonImportTest.kt @@ -14,7 +14,7 @@ import kotlin.test.Test * The whole klib path in one interpreter, on a **Kotlin/Native** target: a `.klib` the build * resolved -> `KlibScanner` -> an (empty) `ArtifactTable` -> `UpcallTable` -> `PythonProxySource`. * - * `:ksp-fixtures:artifact`'s `WalkedArtifactPythonImportTest` made a *positive* version of this claim + * `:python-multiplatform-gradle-plugin-fixtures:artifact`'s `WalkedArtifactPythonImportTest` made a *positive* version of this claim * for a **jar** on **desktop**: a bound declaration reaches Python and is callable. This class makes * the negative half instead -- see `WalkedKlibArtifactTableTest`'s KDoc for why `kotlinx-coroutines * -core`'s klib half of the shared namespace is correctly *empty*, not merely small. Both matter: a diff --git a/ksp-fixtures/klib-artifact/src/androidNativeArm64Test/kotlin/fixture/artifact/WalkedKlibArtifactTableTest.kt b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactTableTest.kt similarity index 64% rename from ksp-fixtures/klib-artifact/src/androidNativeArm64Test/kotlin/fixture/artifact/WalkedKlibArtifactTableTest.kt rename to python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactTableTest.kt index 77c635a40..eb4325e4a 100644 --- a/ksp-fixtures/klib-artifact/src/androidNativeArm64Test/kotlin/fixture/artifact/WalkedKlibArtifactTableTest.kt +++ b/python-multiplatform-gradle-plugin/fixtures/klib-artifact/src/nativeTest/kotlin/fixture/artifact/WalkedKlibArtifactTableTest.kt @@ -21,7 +21,7 @@ import kotlin.test.assertTrue * among them) that `KlibScanner` **used to** bind before this suite ever ran on a device: it is * binary-visible so an inline call site inside `kotlinx-coroutines-core` itself can resolve it, but * not source-visible from a generated fragment in a *different* module, and - * `:ksp-fixtures:klib-artifact:compileKotlinAndroidNativeArm64` failing with "it is internal in file" + * `:python-multiplatform-gradle-plugin-fixtures:klib-artifact:compileKotlinAndroidNativeArm64` failing with "it is internal in file" * -- the first time this walker's generated Kotlin was ever actually compiled rather than only * unit-tested -- is what caught it. `KlibScanWorkAction`'s KDoc has the full story; `KlibScannerTest * .aPublishedApiInternalDeclarationIsDeclinedWithItsOwnReason` pins the fix on the JVM side, and this @@ -55,30 +55,43 @@ class WalkedKlibArtifactTableTest { } /** - * The fixed text: everything `KlibScanner` bound out of the whole `kotlinx-coroutines-core` - * klib under the `kotlinx.coroutines` namespace -- nothing, correctly. This class's own KDoc has - * the reason; `KlibScannerTest.aRealThirdPartyKlibIsScannedWithoutCrashingAndDeclinesEveryDeclaration` - * pins the same fact walking the klib directly rather than through the build. If the two ever - * disagree, the wiring between `KlibScanner` and this generated table is what broke. - * - * A generated fragment file is only ever written when a walked artefact contributes at least one - * entry (`PythonArtifactBindingsTask.generate`'s `if (entries.isEmpty()) return@forEach`), so an - * empty klib means `ArtifactTable.fragments` itself has zero elements -- not one fragment with an - * empty `entries()`. + * The fixed text: everything `KlibScanner` bound out of the whole `kotlinx-coroutines-core` klib + * under the `kotlinx.coroutines` namespace. Zero before issue #234, when no Kotlin object could + * cross the klib boundary; since then the factories whose types are the klib's own classes, and + * since #237 the members of its interfaces and final classes. + * `KlibScannerTest.aRealThirdPartyKlibIsScannedWithoutCrashingAndBindsItsHandleFactoriesAndMembers` pins the + * same list walking the klib directly; if the two disagree, the wiring between `KlibScanner` and + * this generated table is what broke. */ @Test - fun theWalkerCorrectlyDeclinesEveryDeclarationInTheKlib() { - assertEquals(emptyList(), ArtifactTable.fragments) + fun theWalkerBindsExactlyTheKlibsHandleFactoriesAndMembers() { ArtifactTable.registerInto() - assertEquals(emptyList(), UpcallTable.entries()) + assertEquals( + listOf( + "kotlinx.coroutines.ChildJob.parentCancelled", + "kotlinx.coroutines.CompletableJob.complete", + "kotlinx.coroutines.DisposableHandle.dispose", + "kotlinx.coroutines.Job", + "kotlinx.coroutines.Job.attachChild", + "kotlinx.coroutines.Job.start", + "kotlinx.coroutines.MainScope", + "kotlinx.coroutines.Runnable.run", + "kotlinx.coroutines.SupervisorJob", + "kotlinx.coroutines.TimeoutCancellationException.createCopy", + "kotlinx.coroutines.newFixedThreadPoolContext", + "kotlinx.coroutines.newSingleThreadContext", + "kotlinx.coroutines.sync.Mutex", + "kotlinx.coroutines.sync.Semaphore", + "kotlinx.coroutines.sync.Semaphore.release", + "kotlinx.coroutines.sync.Semaphore.tryAcquire", + ), + UpcallTable.entries().map { it.name }.filter { it.startsWith("kotlinx.") }.sorted(), + ) } - /** - * `registerInto` on an empty `ArtifactTable` must not fail, and must not resolve a name that was - * never bound -- the two failure modes a table with zero fragments could otherwise hide. - */ + /** `registerInto` must not resolve a name that was never bound: `checkIndexOverflow` stays declined. */ @Test - fun registeringAnEmptyTableIsHarmlessAndResolvesNothing() { + fun aDeclinedNameStillResolvesToNothing() { ArtifactTable.registerInto() val entry = UpcallTable.resolve("kotlinx.coroutines.flow.internal.checkIndexOverflow") assertTrue(!entry.isValid, "checkIndexOverflow must not resolve: it is @PublishedApi internal and must stay declined") diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt index dce8d74a4..5fef66032 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/IosPythonHome.kt @@ -33,7 +33,7 @@ import java.nio.file.StandardCopyOption * * `build/utils.sh`'s `install_stdlib` merges the shared tree with one `lib-$ARCHS` tree into the app; * this does the same merge at Gradle time, into a directory per slice, so the Xcode phase - * (`tools/xcode/install-python.sh`) copies a finished prefix and needs no path into the archive. + * (`python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`) copies a finished prefix and needs no path into the archive. * Pure functions are separated out so the layout is pinned by tests without downloading anything. */ object IosPythonHomeLayout { @@ -100,7 +100,7 @@ object IosPythonHomeLayout { val platform = effectivePlatformName?.trim()?.removePrefix("-") require(!platform.isNullOrEmpty()) { "EFFECTIVE_PLATFORM_NAME is not set. This task picks the iOS slice from the settings Xcode " + - "exports to a Run Script phase; run it from one (tools/xcode/install-python.sh does)." + "exports to a Run Script phase; run it from one (python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh does)." } val archList = archs?.trim()?.split(Regex("\\s+"))?.filter { it.isNotEmpty() }.orEmpty() require(archList.isNotEmpty()) { "ARCHS is not set; run this from an Xcode Run Script phase." } @@ -219,7 +219,7 @@ abstract class StageIosPythonHomeTask : DefaultTask() { val home = File(root, PREFIX_DIRECTORY) // Whole directory replaced: a file dropped from the plan (an exclusion added, an upstream // module removed) must not survive from the previous run into the next app. - home.deleteRecursively() + deleteWithoutFollowingLinks(home) for ((relative, source) in plan) { val target = File(home, relative) target.parentFile.mkdirs() diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/NoFollowDelete.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/NoFollowDelete.kt new file mode 100644 index 000000000..88d3ab44e --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/NoFollowDelete.kt @@ -0,0 +1,32 @@ +package python.multiplatform.gradle + +import java.io.File +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +/** + * Deletes [path] and everything under it without following symbolic links (issue #227). + * + * Kotlin's `File.deleteRecursively` descends into a directory symlink and deletes what it points + * at. `Files.walkFileTree` without `FOLLOW_LINKS` reports a link as a file-like entry, so the link + * itself is removed and its target is left alone. A missing path is not an error. + */ +internal fun deleteWithoutFollowingLinks(path: File) { + val start = path.toPath() + if (!Files.exists(start, java.nio.file.LinkOption.NOFOLLOW_LINKS)) return + Files.walkFileTree(start, object : SimpleFileVisitor() { + override fun visitFile(file: java.nio.file.Path, attrs: BasicFileAttributes): FileVisitResult { + Files.delete(file) + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: java.nio.file.Path, exc: IOException?): FileVisitResult { + if (exc != null) throw exc + Files.delete(dir) + return FileVisitResult.CONTINUE + } + }) +} diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonBindingsPlugin.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonBindingsPlugin.kt index 01aa4a30c..38bcb9fe1 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonBindingsPlugin.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonBindingsPlugin.kt @@ -12,7 +12,7 @@ import python.multiplatform.gradle.stubs.PythonStubsTask * KSP option names. Copied rather than imported: these are the processor's * `python.multiplatform.ksp.Constants`, and this plugin ships as a separate build so that a * consumer resolves it from the plugin portal without also resolving the processor's compile - * classpath. Changing one without the other shows up in `ksp-fixtures` as an unwired module. + * classpath. Changing one without the other shows up in the `fixtures/` modules as an unwired module. */ private const val OPTION_ROLE = "python.multiplatform.role" private const val OPTION_MODULE_NAME = "python.multiplatform.moduleName" @@ -53,7 +53,7 @@ private const val TYPEDPYTHON_STUBS_CONFIGURATION = "typedpythonStubs" * and a second `FunctionTable` -- into the test compilation, where they shadowed the real ones * from `main`, because a compilation's own generated sources win over its classpath. The symptom * is a table holding the test classes and nothing the module actually exposes. - * `ksp-fixtures/android` is what surfaced this; no module without an Android plugin can. + * `python-multiplatform-ksp/fixtures/android` is what surfaced this; no module without an Android plugin can. * * `Test` is therefore matched as a camel-case *word* rather than as a suffix: it must start a word * and end one. A target or product flavour genuinely named `testing` is not a test compilation and @@ -96,7 +96,7 @@ internal fun deriveModuleName(group: String, path: String, name: String): String * package, so two of them collide. Gradle has no general notion of "the module that ships", and * a Kotlin Multiplatform module that builds a framework or a native executable carries no plugin * that distinguishes it -- hence the explicit `role` override on the extension, which is what - * `ksp-fixtures/app` uses. + * `python-multiplatform-ksp/fixtures/app` uses. */ internal fun inferRole(hasApplicationPlugin: Boolean, hasAndroidApplicationPlugin: Boolean): String = if (hasApplicationPlugin || hasAndroidApplicationPlugin) ROLE_APP else ROLE_LIBRARY @@ -121,7 +121,7 @@ interface PythonBindingsExtension { /** * Whether to stage a CPython prefix for this machine and point `PYTHONHOME` at it on every - * `JavaExec` and `Test` task. Defaults to true — see [PythonHomeStaging] for why the library + * `JavaExec` and `Test` task. Defaults to true, see [PythonHomeStaging] for why the library * cannot supply one at runtime, and ROADMAP §15e item 4 for the gap it closes. * * It is already a no-op whenever `PYTHONHOME` is set in the environment, so setting this to @@ -145,6 +145,14 @@ interface PythonBindingsExtension { */ val packagePythonHome: Property + /** + * SPEC L-9 (issue #246): the directory a GraalVM native image of this app is written to. When it + * is set, `stageNativeImagePythonHome` copies the same pruned prefix Compose packaging gets into + * `/python-multiplatform-home`, where the library finds it at run time beside the + * executable. Unset, the task copies nothing. + */ + val nativeImageDirectory: org.gradle.api.file.DirectoryProperty + /** CPython version to stage. Defaults to the one this library was built against. */ val pythonVersion: Property @@ -186,7 +194,7 @@ interface PythonBindingsExtension { * Named explicitly rather than derived, because this plugin carries no Kotlin Gradle Plugin * types (see [TEST_WORD]) and so cannot ask a target what its compile classpath is called. One * configuration and one source set is also the honest scope of what has been verified: see - * `ksp-fixtures/artifact`, and ROADMAP §16 for the generalisation. + * `python-multiplatform-gradle-plugin/fixtures/artifact`, and ROADMAP §16 for the generalisation. */ val artifactConfiguration: Property @@ -362,6 +370,7 @@ class PythonBindingsPlugin : Plugin { }, ) includePrefixes.set(includes) + excludePackages.set(extension.excludePackages.getOrElse(emptyList())) this.klibReaderClasspath.from(klibReader) outputDirectory.set( project.layout.buildDirectory.dir("generated/pythonArtifactBindings/$sourceSetName"), @@ -508,6 +517,7 @@ class PythonBindingsPlugin : Plugin { description = "Emits .pyi stubs for the declarations the artefact walker bound." artifacts.from(resolved.artifactFiles) includePrefixes.set(includes) + excludePackages.set(extension.excludePackages.getOrElse(emptyList())) klibReaderClasspath.from(klibReader) outputDirectory.set(project.layout.buildDirectory.dir("generated/pythonStubs/$sourceSetName")) } @@ -571,7 +581,7 @@ class PythonBindingsPlugin : Plugin { * The prefix path is computed here rather than carried as a `Provider`, and it can be: it is a * pure function of the version, the release, the host and the Gradle user home, none of which * need the task to have run. That keeps `environment(...)` a plain string set at configuration - * time — the value is in the child process's environment before its JVM starts, which is the + * time, the value is in the child process's environment before its JVM starts, which is the * only way CPython's `getenv(3)` and [python.multiplatform.env.PythonHomeCheck]'s * `System.getenv` can be guaranteed to read the same thing. * @@ -682,6 +692,21 @@ class PythonBindingsPlugin : Plugin { requireNoEmptyLibraries(destinationDir.resolve(PACKAGED_HOME_DIRECTORY)) } } + + // SPEC L-9 (issue #246): the same prefix beside a native image, wherever the app writes one. + val besideImage = project.provider { extension.nativeImageDirectory.isPresent } + project.tasks.register("stageNativeImagePythonHome", org.gradle.api.tasks.Sync::class.java) { + group = "python" + description = "Copies the CPython prefix beside a GraalVM native image (pythonBindings.nativeImageDirectory)." + dependsOn(java.util.concurrent.Callable { if (besideImage.get()) listOf(stage) else emptyList() }) + from(java.util.concurrent.Callable { if (besideImage.get()) stagedPrefix else emptyList() }) { + include(includes) + exclude(PACKAGED_PREFIX_EXCLUDES) + } + into(extension.nativeImageDirectory.dir(PACKAGED_HOME_DIRECTORY).orElse(project.layout.buildDirectory.dir("tmp/stageNativeImagePythonHome-unset"))) + onlyIf("pythonBindings.nativeImageDirectory is set") { besideImage.get() } + doLast { requireNoEmptyLibraries(destinationDir) } + } } /** diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonHomeStaging.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonHomeStaging.kt index ce08d41c5..a952d6f1d 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonHomeStaging.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonHomeStaging.kt @@ -15,14 +15,14 @@ import java.security.MessageDigest import javax.inject.Inject /** - * Staging a CPython prefix for a consumer's build — ROADMAP §15e item 4. + * Staging a CPython prefix for a consumer's build, ROADMAP §15e item 4. * * ### The gap this closes * * §15c published the library to `mavenLocal()` and built a genuine external JVM consumer against * it. Everything worked *except* start-up: `desktopJar` carries `libpython` for four host * platforms under `lib//`, and `manager.loadLibPython` finds it there with no wiring at - * all — but it carries **no standard library**, and `Py_Initialize()` dies with + * all, but it carries **no standard library**, and `Py_Initialize()` dies with * `Fatal Python error: Failed to import encodings module` until `PYTHONHOME` names a prefix that * has one. Verified rather than read: the published jar's `lib/` tree is 14 entries, every one of * them a shared library. @@ -45,17 +45,17 @@ import javax.inject.Inject * instead of being multiplied into an artifact every consumer downloads. That matters here more * than usual: `desktopJar` is already 87.7 MB for four platforms' `libpython`, and the four * platforms' stdlibs measure a further 43.5 MB compressed (macos-aarch64 8.1, macos-x86_64 7.1, - * linux-x86_64 8.2, windows-x86_64 20.1) — for payload of which any one consumer uses exactly + * linux-x86_64 8.2, windows-x86_64 20.1), for payload of which any one consumer uses exactly * one quarter. Putting per-platform payload into the artifact every platform resolves is the * defect §15d diagnosed when `allMetadataJar` reached 87.4 MB, and it was fixed by *narrowing* * scope, not by accepting the size. * - **`PYTHONHOME` cannot be set from inside a running JVM.** CPython reads it with `getenv(3)`, * from the native process environment. Android's helper calls `Os.setenv`; the JVM has no - * equivalent — `System.getenv` is an immutable snapshot taken at start-up, and changing it (by + * equivalent, `System.getenv` is an immutable snapshot taken at start-up, and changing it (by * reflection or otherwise) does not touch the native environment CPython actually reads. A * runtime helper would therefore have to reach libc's `setenv` through Panama, which is a * different symbol on Windows (`_putenv_s`), and would then be setting a value that - * [python.multiplatform.env.PythonHomeCheck] — which reads `System.getenv` — could no longer + * [python.multiplatform.env.PythonHomeCheck], which reads `System.getenv`, could no longer * see. Setting it when the *process is launched*, which is what Gradle's `environment(...)` * does, has neither problem: CPython and `PythonHomeCheck` read the same value from the same * place, exactly as they already do for this repository's own `desktopTest`. @@ -95,7 +95,7 @@ internal object PythonHomeStaging { * `linux-aarch64` is included even though `desktopJar` carries no `libpython` for it, and that is * not an oversight: `manager.loadFromSidecar` falls back to loading the interpreter *out of * `PYTHONHOME`* whenever the classpath has no library for the running platform, so a staged prefix - * is the whole of what that host needs. Staging is the only reason it can work at all — this is + * is the whole of what that host needs. Staging is the only reason it can work at all, this is * not verified on such a host, see this file's own note in ROADMAP §15g. */ internal fun hostDesktopPlatform(osName: String, osArch: String): String? { @@ -172,8 +172,8 @@ internal fun sha256Of(sha256sums: String, assetName: String): String? = /** * A file that must be readable under a prefix for it to be usable, relative to the prefix. * - * This is exactly what `PythonHomeCheck.diagnose` probes — `lib/python/os.py`, or `Lib/os.py` - * on the layout CPython's Windows build produces — so a prefix this staging accepts is one + * This is exactly what `PythonHomeCheck.diagnose` probes, `lib/python/os.py`, or `Lib/os.py` + * on the layout CPython's Windows build produces, so a prefix this staging accepts is one * `Python3.initialize()` accepts. Asserting a named file rather than "the directory is not empty" * is §15f's lesson: the root build's own extraction step skips when `extractDir.list()` is * non-empty, which is true of a tree whose extraction was interrupted. @@ -190,7 +190,7 @@ internal fun stdlibMarkerRelativePath(pythonVersion: String, freeThreaded: Boole * Identifies precisely which distribution was extracted. * * Written after the last byte and deleted before a rewrite begins, so it cannot be true of a - * partial tree — §15f found three hand-written Android copies that each probed a single entry of + * partial tree, §15f found three hand-written Android copies that each probed a single entry of * the result and so accepted a tree whose copy had been interrupted. * * It names the upstream release as well as the Python version because astral republishes the same @@ -397,8 +397,8 @@ internal fun isComposeAppResourcesTask(name: String): Boolean = /** * Whether the plugin should stage a prefix and set `PYTHONHOME`. * - * A consumer who has already set `PYTHONHOME` — at a system CPython, at a conda prefix, at a build - * they compiled themselves — keeps it. Replacing it would turn a working setup into a + * A consumer who has already set `PYTHONHOME`, at a system CPython, at a conda prefix, at a build + * they compiled themselves, keeps it. Replacing it would turn a working setup into a * differently-configured one on a library upgrade, and §15c's whole finding is that setting it by * hand is what consumers have had to do until now. */ diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt index b4d83da2f..8ba46eca6 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmBrowserRuntimeStaging.kt @@ -25,7 +25,7 @@ import javax.inject.Inject * exists only inside this repository's checkout. An external consumer has no such directory and no * way to get one -- the CPython build it names is produced by hand-patching CPython's own source * (`Lib/sysconfig`, the generated `Makefile`) against a specific Emscripten SDK version, which is - * not something any upstream publishes yet. See `tools/wasm/build-cpython.sh`'s own header for why: PEP + * not something any upstream publishes yet. See `python-multiplatform/scripts/wasm/build-cpython.sh`'s own header for why: PEP * 783's `pyemscripten_2026_0` tag is real and accepted, but nothing ships a `python.wasm` whose * `EXPORTED_RUNTIME_METHODS` include `wasmExports`/`wasmMemory` -- that addition exists only so this * library's `@WasmImport` declarations have something to bind to, and no distributor has a reason to diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmProxyExportsRendering.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmProxyExportsRendering.kt index b64e9f0a9..bf7653a52 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmProxyExportsRendering.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/WasmProxyExportsRendering.kt @@ -29,7 +29,7 @@ internal const val WASM_PROXY_EXPORTS_FILE_NAME = "WasmProxyTypeExports" * ### Why this must be generated into the executable, not the library * * `@WasmExport` is honoured only in the compilation that produces the `.wasm` binary. A library - * klib linked in does not carry the annotation into the binary's export section — measured, not + * klib linked in does not carry the annotation into the binary's export section, measured, not * assumed: the identical annotation on the identical function exported from `wasmJsTest` and did * not export from `wasmJsMain`. So the three lines cannot live in the library; they must be * emitted into each executable compilation that embeds it. @@ -45,9 +45,9 @@ internal const val WASM_PROXY_EXPORTS_FILE_NAME = "WasmProxyTypeExports" * exactly match the C slot signature it fills raises `RuntimeError: null function or function * signature mismatch` at runtime. The three C signatures are: * - * int tp_traverse(PyObject *self, visitproc visit, void *arg) — (i32,i32,i32) -> i32 - * int tp_clear (PyObject *self) — (i32) -> i32 - * void tp_dealloc (PyObject *self) — (i32) -> () + * int tp_traverse(PyObject *self, visitproc visit, void *arg) , (i32,i32,i32) -> i32 + * int tp_clear (PyObject *self) , (i32) -> i32 + * void tp_dealloc (PyObject *self) , (i32) -> () * * [packageName] is the package the generated file is placed in, matching the executable module's * choice. It is the only variable part of the output. diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/ArtifactScanner.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/ArtifactScanner.kt index 23dfffb47..427dbcbc7 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/ArtifactScanner.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/ArtifactScanner.kt @@ -4,7 +4,9 @@ import kotlin.metadata.ClassKind import kotlin.metadata.KmClassifier import kotlin.metadata.KmType import kotlin.metadata.KmVariance +import kotlin.metadata.MemberKind import kotlin.metadata.Visibility +import kotlin.metadata.isFunInterface import kotlin.metadata.isNullable import kotlin.metadata.isSuspend import kotlin.metadata.isValue @@ -42,7 +44,7 @@ import java.util.jar.JarFile * * | PyREPL's filter | here | why | * |---|---|---| - * | not `private`/`protected` | **not enough** — must be `public`, and Kotlin-`public` rather than only JVM-`public` | PyREPL keeps package-private members, which is harmless in a stub; `internal` is JVM-public but not a name generated Kotlin may call | + * | not `private`/`protected` | **not enough**, must be `public`, and Kotlin-`public` rather than only JVM-`public` | PyREPL keeps package-private members, which is harmless in a stub; `internal` is JVM-public but not a name generated Kotlin may call | * | drop `` | kept | a constructor needs a `ReflectedClass` and a receiver handle, which is the next step | * | drop `Companion` | subsumed | only statics are bound, and `Companion` is an instance field | * | drop names containing `-` | **not kept** | see below | @@ -196,9 +198,16 @@ internal object ArtifactScanner { * test fixtures, false for `androidx.compose.foundation.layout`, whose `Dp` lives in * `androidx.compose.ui.unit`. [PythonArtifactBindingsTask] passes the whole resolved * configuration. + * @param excludePackages packages to leave out, on top of every `internal` package (SPEC B-11): + * an entry covers the package it names and the ones below it. */ - fun scanJar(target: File, includePrefixes: List = emptyList(), classpath: List = listOf(target)): List = - scan(target, includePrefixes, classpath).mapNotNull { it.callable }.sortedBy { it.name } + fun scanJar( + target: File, + includePrefixes: List = emptyList(), + classpath: List = listOf(target), + excludePackages: List = emptyList(), + ): List = + scan(target, includePrefixes, classpath, excludePackages).mapNotNull { it.callable }.sortedBy { it.name } /** * The same walk, with the half `docs/design/pyi-generation-design.md` §2.2 asks for kept: one @@ -214,11 +223,17 @@ internal object ArtifactScanner { target: File, includePrefixes: List = emptyList(), classpath: List = listOf(target), - ): List = scan(target, includePrefixes, classpath) + excludePackages: List = emptyList(), + ): List = scan(target, includePrefixes, classpath, excludePackages) .map { it.declaration } .sortedWith(compareBy({ it.owner }, { it.simpleName }, { it.bindingName ?: "" })) - private fun scan(target: File, includePrefixes: List, classpath: List): List { + private fun scan( + target: File, + includePrefixes: List, + classpath: List, + excludePackages: List, + ): List { val artifactClasspath = ArtifactClasspath(classpath) val entries = mutableListOf() forEachClassEntry(target) { relativePath, bytes -> @@ -228,6 +243,7 @@ internal object ArtifactScanner { if ('$' in relativePath) return@forEachClassEntry val qualified = binaryNameToQualified(relativePath.removeSuffix(".class")) if (!matchesInclude(qualified, includePrefixes)) return@forEachClassEntry + if (isExcludedPackage(packageNameOf(relativePath.removeSuffix(".class")), excludePackages)) return@forEachClassEntry val node = readClassNodeOrNull(bytes) ?: return@forEachClassEntry entries += scanClassNode(node, artifactClasspath) } @@ -246,9 +262,18 @@ internal object ArtifactScanner { // neither claims a constructor's namespace (`TextStyle.color` must not cost `TextStyle(...)` // its binding) nor joins an overload set. [settlePropertyNames] gives them the one check // they do need, against the finished names of everything else. - val (properties, functions) = entries.partition { it.declaration.kind in PROPERTY_KINDS } + // + // Member functions (issue #215) take no part in them either, for the property's reason: a + // `METHOD` is served on its receiver's proxy, so `Counter.add` must not cost `Counter(...)` its + // constructor (the prefix rule of [dropCollidingConstructors]), and a member is never an + // overload of a top-level function of the same name. Inside one class they are an overload set + // like any other, so [disambiguateOverloads] still names them; [settleMethodNames] is their one + // check against everything else. + val (properties, rest) = entries.partition { it.declaration.kind in PROPERTY_KINDS } + val (methods, functions) = rest.partition { it.declaration.kind == METHOD_KIND } val settledFunctions = assignThunkIndices(disambiguateOverloads(applyDefaultOmission(dropCollidingConstructors(functions)))) - return (settledFunctions + settlePropertyNames(properties, settledFunctions)) + val settledMethods = settleMethodNames(disambiguateOverloads(methods), settledFunctions) + return (settledFunctions + settledMethods + settlePropertyNames(properties, settledFunctions + settledMethods)) // The ancestry the binding carries is the model's too (issue #31): copied at the end, from the // callable that was actually bound, so the two cannot be read differently. .map { candidate -> @@ -259,8 +284,9 @@ internal object ArtifactScanner { // unwritable name, an ambiguous sibling): the binding then requires every argument // while the stub would still offer `= ...`. One view, taken from the binding. val omittable = omittableSlotsOf(callable) - // A property's receiver is not one of its slots (`CallableKind.GETTER.hasReceiver`). - val receiverIndex = if (callable.receiverTypeName != null && callable.kind !in PROPERTY_KINDS) 1 else 0 + // A property's or a member's receiver is not one of its slots (`CallableKind.hasReceiver`); + // only an extension's is. + val receiverIndex = if (callable.receiverTypeName != null && callable.kind == "FUNCTION") 1 else 0 candidate.copy( declaration = candidate.declaration.copy( returnSupertypes = callable.returnSupertypes, @@ -380,6 +406,15 @@ internal object ArtifactScanner { private fun packageNameOf(binaryName: String): String = binaryNameToQualified(binaryName.substringBeforeLast('/', missingDelimiterValue = "")) + /** + * SPEC B-11: a package is out when one of its segments is exactly `internal` (`*.internal`, + * `*.internal.*`, the Kotlin `.api` dump convention) or when it equals, or lies below, an + * `excludePackages` entry (the KSP side's namespace rule). Shared with [KlibScanner]. + */ + internal fun isExcludedPackage(packageName: String, excludePackages: List): Boolean = + packageName.split('.').any { it == "internal" } || + excludePackages.any { packageName == it || packageName.startsWith("$it.") } + /** A namespace match, not a text one: `junit.run` is a prefix of the string `junit.runner` and * of no package or class. */ private fun matchesInclude(qualifiedName: String, includePrefixes: List): Boolean = @@ -410,7 +445,8 @@ internal object ArtifactScanner { bindObjectMembers = metadata.kmClass.visibility == Visibility.PUBLIC, ) + constructorCandidates(metadata.kmClass, node, classpath) + objectConstantCandidates(metadata.kmClass, node, classpath) + - memberPropertyCandidates(metadata.kmClass, node, classpath) + memberPropertyCandidates(metadata.kmClass, node, classpath) + + memberFunctionCandidates(metadata.kmClass, node, classpath) is KotlinClassMetadata.FileFacade -> { val owner = kotlinPackageNameOverrideOf(node) ?: packageNameOf(node.name) kotlinCandidates( @@ -541,7 +577,8 @@ internal object ArtifactScanner { * group. */ private fun ArtifactCallable.overloadSuffix(includeReceiver: Boolean, qualified: Boolean): String { - val skipReceiver = if (receiverTypeName != null && !includeReceiver) 1 else 0 + // A member's receiver (issue #215) is not in [paramTypeNames] at all, so there is nothing to skip. + val skipReceiver = if (receiverTypeName != null && kind == "FUNCTION" && !includeReceiver) 1 else 0 // A composable's synthetic slots are the same for every member of its overload set -- // `Composer_Int_Int_Int` on all of them -- so they separate nothing and would only make the // Python name unreadable. Dropped, which leaves exactly the declared parameters the two @@ -551,7 +588,9 @@ internal object ArtifactScanner { // A zero-parameter member of a group -- there can be at most one, so this only ever has to // be distinct from the others, not descriptive. if (parts.isEmpty()) return "0" - return parts.joinToString("_") { name -> + return parts.joinToString("_") { slotName -> + // A functional-interface slot (issue #216) is named by its interface, not by the callable form. + val name = slotName.substringBefore(SAM_SEPARATOR) val chosen = if (qualified) name else name.substringAfterLast('.') chosen.map { if (it.isLetterOrDigit()) it else '_' }.joinToString("") } @@ -821,6 +860,121 @@ internal object ArtifactScanner { // -------------------------------------------------------------------------- properties (#38) + /** What a member function's candidate needs of the class it is read on (issue #215). */ + private class MemberReceiver( + /** The class's qualified Kotlin name: the entry's `receiverTypeName` and the key's owner. */ + val owner: String, + /** The receiver as generated source reads it out of `args[0]`. */ + val read: String, + private val model: KotlinTypeModel, + ) { + /** [declaration] as a `METHOD`: read off a receiver of [owner], every argument passed. */ + fun restate(declaration: DeclarationModel): DeclarationModel = declaration.copy( + kind = METHOD_KIND, + receiver = model, + receiverBoundaryTag = OBJECT_TAG, + parameters = declaration.parameters.map { it.copy(declaresDefault = false) }, + ) + } + + /** + * A public class's or interface's own **member functions**, as `METHOD` entries under + * `Owner.name` (issue #215). + * + * `kotlinx.coroutines.Job.cancel()` and `CoroutineDispatcher.dispatch(...)` are members: until this + * producer, a handle Python held to one could have its properties read and nothing called on it. + * `CallableKind.METHOD` already says the shape -- `args[0]` is the receiver, resolved from the + * proxy's handle and not counted in `arity` -- and KSP already emits it for its own classes, so + * nothing about the boundary is new. The call is source, `(args[0] as Owner).name(...)`; `kotlinc` + * resolves and mangles it. + * + * ### What is bound + * + * The class rules are [memberPropertyCandidates]': Kotlin-public, a `class`, `interface` or `enum + * class` (an `object`'s functions are #53's statics), crossing as an object handle, every supertype + * on the walk's classpath. A function is bound when it is public, declared rather than synthesized + * (a data class's `copy`/`componentN`, U-4), not an extension (a member extension needs two + * receivers), not one of `Any`'s members, and backed by a public, non-static, non-synthetic JVM + * method whose parameter count is the declared one. Declined, each with its reason: `suspend`, a + * reified or bounded type parameter, `@Composable`, a member of a generic class (its receiver would + * be cast with arguments a type parameter of the function may need -- not yet), and a member that + * shares its name with a property of the class, because Python has one attribute for both and the + * property (#38) keeps it. + */ + private fun memberFunctionCandidates( + kmClass: kotlin.metadata.KmClass, + ownerNode: ClassNode, + classpath: ArtifactClasspath, + ): List { + if (kmClass.visibility != Visibility.PUBLIC) return emptyList() + if (kmClass.kind !in PROPERTY_OWNER_KINDS) return emptyList() + val synthesized = kmClass.functions.filter { it.kind == MemberKind.SYNTHESIZED }.mapTo(HashSet()) { it.name } + val functions = functionsOf(kmClass).filter { !it.isExtension && it.kotlinName !in synthesized } + if (functions.isEmpty()) return emptyList() + val owner = binaryNameToQualified(ownerNode.name) + val ownerType = KmType().apply { classifier = KmClassifier.Class(ownerNode.name) } + val receiverBoundary = resolveKotlinBoundary(ownerType, classpath, BoundaryDirection.PARAMETER) ?: return emptyList() + if (receiverBoundary.boundary.tag != OBJECT_TAG) return emptyList() + val receiverModel = kotlinTypeModelOf(ownerType, classpath) ?: return emptyList() + val member = MemberReceiver(owner, receiverBoundary.boundary.read("args[0]"), receiverModel) + val receiverDecline = classpath.unreachableSupertypeOf(ownerNode.name) + ?.let { missing -> unreachableReceiverReason(missing, owner) } + val propertyNames = kmClass.properties.mapTo(HashSet()) { it.name } + val methods = ownerNode.methods.associateBy { it.name to it.desc } + + return functions.mapNotNull { function -> + val signature = function.jvmSignature.name to function.jvmSignature.descriptor + if (signature in ANY_MEMBER_SIGNATURES) return@mapNotNull null + val method = methods[signature] + val composable = method != null && isComposable(method) + fun declined(reason: String) = + declinedCandidate(owner, true, function, classpath, reason, composable) + ?.let { it.copy(declaration = member.restate(it.declaration)) } + when { + function.isSuspend -> return@mapNotNull declined("suspend") + function.hasReifiedTypeParameter -> + return@mapNotNull declined("a reified type parameter: its type argument is read at run time, so kotlin.Any? cannot stand for it") + method == null || !method.access.hasFlag(Opcodes.ACC_PUBLIC) -> return@mapNotNull null + method.access.hasFlag(Opcodes.ACC_STATIC) || method.access.hasFlag(Opcodes.ACC_SYNTHETIC) || + method.access.hasFlag(Opcodes.ACC_BRIDGE) -> return@mapNotNull null + composable -> return@mapNotNull declined("a @Composable member function: not bound on a proxy") + kmClass.typeParameters.isNotEmpty() -> return@mapNotNull declined("a member of a generic class: not bound yet") + function.kotlinName in propertyNames -> + return@mapNotNull declined("shares its name with a property of the class, which keeps the attribute") + receiverDecline != null -> return@mapNotNull declined(receiverDecline) + } + val (paramDescriptors, _) = splitMethodDescriptor(method!!.desc) + if (function.allParameterTypes.size != paramDescriptors.size) { + return@mapNotNull declined( + "the JVM signature carries ${paramDescriptors.size - function.allParameterTypes.size} " + + "parameter(s) metadata does not declare", + ) + } + candidateFromFunction(owner, true, function, classpath, paramDescriptors, isComposable = false, member = member) + } + } + + /** + * Gives a member function (issue #215) a key no other binding holds. Keys are `Owner.name`, the + * namespace of a property's (`Owner.prop`) and of an object's constants and functions; a property + * of the same name is already excluded where the member is built, so what is left here is a nested + * object's or a companion's binding under the same dotted name. The other binding keeps it. + */ + private fun settleMethodNames(methods: List, settled: List): List { + val taken = settled.mapNotNullTo(HashSet()) { it.callable?.name } + return methods.map { candidate -> + val name = candidate.callable?.name ?: return@map candidate + if (name !in taken) return@map candidate + candidate.copy( + callable = null, + declaration = candidate.declaration.copy( + bindingName = null, + declineReason = "member key $name collides with another binding", + ), + ) + } + } + /** * A public class's or interface's own **member** properties, as `GETTER` entries -- and, for a * `var` whose setter is public, `SETTER` entries -- under `Owner.prop` and `Owner.prop=`. @@ -1180,6 +1334,7 @@ internal object ArtifactScanner { "so generated Kotlin cannot read a member of it" internal const val GETTER_KIND = "GETTER" + internal const val METHOD_KIND = "METHOD" internal const val SETTER_KIND = "SETTER" /** The two kinds [scan] keeps out of the function passes; see [settlePropertyNames]. */ @@ -1607,6 +1762,9 @@ internal object ArtifactScanner { val typeName: String? = null, val boundary: BoundaryType? = null, val declineReason: String? = null, + /** The type a stub shows for the slot when it is not the declared one: a functional + * interface's `kotlin.FunctionN` (issue #216). */ + val modelType: KotlinTypeModel? = null, ) /** @@ -1642,7 +1800,7 @@ internal object ArtifactScanner { * *different* limit from the one this method opens, which is why it is named rather than left * to fail as a compile error in somebody's generated fragment. * - **`@Composable`, inside a declaration that is not.** `Modifier.composed(factory:)` is the - * case. The fragment is compiled without the Compose plugin (`:ksp-fixtures:artifact`'s + * case. The fragment is compiled without the Compose plugin (`:python-multiplatform-gradle-plugin-fixtures:artifact`'s * `build.gradle.kts` says so, and it is the whole shape of that fixture's claim), so * `@Composable Modifier.() -> Modifier` written there is an ordinary `Function1` while the slot * is a lowered `Function3`. **This one does not fail as a compile error**, which is why it is @@ -1699,6 +1857,142 @@ internal object ArtifactScanner { return FunctionSlot(typeName = name, boundary = BoundaryType("OBJECT", "(%s as $rendered)", "(%s)")) } + /** + * A parameter typed as a **functional interface** (issue #216): a Java interface with one abstract + * method (`java.lang.Runnable`) or a Kotlin `fun interface`, whose method returns `Unit` and which is + * not generic. `null` for anything else, which leaves the parameter exactly as it was -- an + * interface on the classpath still crosses as a handle (`Alignment` is a `fun interface` whose + * method returns an `IntOffset`, and its slots must keep taking `Alignment.Center`). + * + * The slot takes **both** a Python callable and a Kotlin handle. Its type name is the interface's, + * then [SAM_SEPARATOR], then the `kotlin.FunctionN` it is equivalent to + * (`java.lang.Runnable|kotlin.Function0()->kotlin.Unit`): the binding layer fills it from a callable + * through the wrapper path a function type has (B-5, `pythonx._make_function`) and checks a handle + * against the interface. The generated read converts a wrapper with a SAM lambda and casts anything + * else -- `((%s).let { v -> if (v is kotlin.Function0<*>) java.lang.Runnable { ... } else v as + * java.lang.Runnable })` -- which `kotlinc` compiles; nothing is looked up at run time (AGENTS.md §12). + * + * The interface is read off the consumer's classpath, or, for a `java.`/`javax.` one, off the JDK + * this build runs on ([jdkClassNode]): the walk's classpath is the consumer's jars and never held + * `java.lang.Runnable`, which is why `runRunnable(block: Runnable)` was declined. + */ + private fun functionalInterfaceSlotOrNull(type: KmType, classpath: ArtifactClasspath): FunctionSlot? { + val classifier = type.classifier as? KmClassifier.Class ?: return null + val internal = classifier.name + if (internal.startsWith(KOTLIN_FUNCTION_INTERNAL_PREFIX)) return null + if (type.arguments.isNotEmpty()) return null + val binary = internal.replace('.', '$') + val node = classpath.classNode(binary) ?: jdkClassNode(binary) ?: return null + if (!node.access.hasFlag(Opcodes.ACC_INTERFACE) || !node.access.hasFlag(Opcodes.ACC_PUBLIC)) return null + if (node.signature?.startsWith("<") == true) return null + val kmClass = when (val metadata = kotlinClassMetadataOf(node)) { + null -> null + is KotlinClassMetadata.Class -> metadata.kmClass.takeIf { it.isFunInterface } ?: return null + else -> return null + } + val method = abstractMethodsOf(node, classpath)?.singleOrNull() ?: return null + val (descriptors, returns) = splitMethodDescriptor(method.desc) + if (returns != "V") return null + // A Kotlin `fun interface` says each parameter's nullability; a Java one does not, so an object + // parameter is taken as nullable there -- Java may pass `null`, and a non-null type would throw. + val parameterTypes: List> = if (kmClass != null) { + val declared = kmClass.functions.singleOrNull { it.name == method.name } ?: return null + if (declared.valueParameters.size != descriptors.size) return null + declared.valueParameters.map { parameter -> + val name = kotlinClassifierNameOf(parameter.type) ?: return null + name to (renderKotlinSourceType(parameter.type, classpath, depth = 0) ?: return null) + } + } else { + descriptors.map { descriptor -> javaParameterTypeOf(descriptor, classpath) ?: return null } + } + val qualified = internal.replace('/', '.') + val arity = parameterTypes.size + val functionType = "kotlin.Function$arity<" + (parameterTypes.map { it.second } + UNIT_TYPE_NAME).joinToString(", ") + ">" + val star = "kotlin.Function$arity<" + List(arity + 1) { "*" }.joinToString(", ") + ">" + val names = (0 until arity).map { "p$it" } + val arrow = if (arity == 0) "" else names.joinToString(", ", postfix = " -> ") + val convert = "v -> if (v is $star) $qualified { $arrow(v as $functionType)(${names.joinToString(", ")}) } else v as $qualified" + val read = if (type.isNullable) "((%s)?.let { $convert })" else "((%s).let { $convert })" + return FunctionSlot( + typeName = qualified + SAM_SEPARATOR + KOTLIN_FUNCTION_PREFIX + arity + + "(" + parameterTypes.joinToString(",") { it.first } + ")" + FUNCTION_RETURNS + UNIT_TYPE_NAME, + boundary = BoundaryType("OBJECT", read, "(%s)"), + modelType = KotlinTypeModel( + "kotlin.Function$arity", + isNullable = type.isNullable, + arguments = parameterTypes.map { KotlinTypeModel(it.first) } + KotlinTypeModel(UNIT_TYPE_NAME), + ), + ) + } + + /** Separates a functional-interface slot's interface from the function type a callable fills it as + * (issue #216). Must match `pythonx`'s `_SAM_SEPARATOR`; not a character a classifier can contain. */ + internal const val SAM_SEPARATOR: String = "|" + + /** `Object`'s public methods, which an interface may redeclare without becoming non-functional. */ + private val OBJECT_METHOD_SIGNATURES = setOf( + "equals" to "(Ljava/lang/Object;)Z", + "hashCode" to "()I", + "toString" to "()Ljava/lang/String;", + ) + + /** + * The abstract methods an interface leaves to an implementation, its superinterfaces' included and + * minus any a subinterface gives a default body; `null` when a superinterface cannot be read. + */ + private fun abstractMethodsOf(node: ClassNode, classpath: ArtifactClasspath): List? { + val abstract = LinkedHashMap, org.objectweb.asm.tree.MethodNode>() + val concrete = HashSet>() + fun visit(current: ClassNode, depth: Int): Boolean { + if (depth > 8) return false + current.methods.filter { !it.access.hasFlag(Opcodes.ACC_STATIC) && !it.access.hasFlag(Opcodes.ACC_SYNTHETIC) }.forEach { method -> + val key = method.name to method.desc + if (key in OBJECT_METHOD_SIGNATURES || method.name.startsWith("<")) return@forEach + if (method.access.hasFlag(Opcodes.ACC_ABSTRACT)) abstract.putIfAbsent(key, method) else concrete += key + } + return current.interfaces.all { parent -> + val parentNode = classpath.classNode(parent) ?: jdkClassNode(parent) ?: return false + visit(parentNode, depth + 1) + } + } + if (!visit(node, 0)) return null + return abstract.filterKeys { it !in concrete }.values.toList() + } + + /** + * A Java method parameter as `(slot type name, Kotlin source type)`: primitives and `String` by + * their Kotlin names, any other class by its name when it is nameable, an object type nullable. + */ + private fun javaParameterTypeOf(descriptor: String, classpath: ArtifactClasspath): Pair? { + val primitive = when (descriptor) { + "I" -> "kotlin.Int"; "J" -> "kotlin.Long"; "Z" -> "kotlin.Boolean"; "D" -> "kotlin.Double" + "F" -> "kotlin.Float"; "S" -> "kotlin.Short"; "B" -> "kotlin.Byte"; "C" -> "kotlin.Char" + else -> null + } + if (primitive != null) return primitive to primitive + if (!descriptor.startsWith("L") || !descriptor.endsWith(";")) return null + val binary = descriptor.substring(1, descriptor.length - 1) + val name = when (binary) { + "java/lang/String" -> "kotlin.String" + "java/lang/Object" -> "kotlin.Any" + else -> if (classpath.isNameablePublicClass(binary)) binary.replace('/', '.').replace('$', '.') else return null + } + return name to "$name?" + } + + /** + * A `java.`/`javax.` class file, read from the JDK this build runs on -- the walk's classpath is the + * consumer's jars, which never contain the platform. Build time only, and only to recognise a + * functional interface ([functionalInterfaceSlotOrNull]); it does not make a JDK type nameable as a + * handle anywhere else. + */ + private fun jdkClassNode(binaryName: String): ClassNode? { + if (!binaryName.startsWith("java/") && !binaryName.startsWith("javax/")) return null + val bytes = ClassLoader.getPlatformClassLoader().getResourceAsStream("$binaryName.class")?.use { it.readBytes() } + ?: return null + return readClassNodeOrNull(bytes) + } + /** The Kotlin-source spelling of a function type: `kotlin.Function1`. * Written as the `FunctionN` classifier rather than as `(Float) -> Unit` because the two are the * same type and the first needs no bracket rules; assignability to an extension function type @@ -1949,8 +2243,11 @@ internal object ArtifactScanner { classpath: ArtifactClasspath, paramDescriptors: List, isComposable: Boolean, + member: MemberReceiver? = null, ): Candidate? { - val model = declarationModelOf(owner, ownerIsClass, function, classpath, isComposable) ?: return null + val model = declarationModelOf(owner, ownerIsClass, function, classpath, isComposable) + ?.let { if (member != null) member.restate(it) else it } + ?: return null val declined = { reason: String -> Candidate(null, model.copy(declineReason = reason)) } val receiverIndex = if (function.isExtension) 1 else 0 if (function.hasUnsubstitutableTypeParameters) { @@ -1971,7 +2268,7 @@ internal object ArtifactScanner { // Read before `resolveKotlinType` is asked anything, because for a function-typed parameter // it is the one that would answer -- and would answer "no". See [functionSlotOrNull]. val functionSlots = function.allParameterTypes.mapIndexed { index, type -> - functionSlotOrNull(type, paramDescriptors[index], classpath) + functionSlotOrNull(type, paramDescriptors[index], classpath) ?: functionalInterfaceSlotOrNull(type, classpath) } functionSlots.firstNotNullOfOrNull { it?.declineReason }?.let { return declined(it) } @@ -1995,7 +2292,10 @@ internal object ArtifactScanner { val returnTypeName = kotlinClassifierNameOf(function.returnType) val qualifiedName = "$owner.${function.kotlinName}" - val argumentExpressions = resolvedParams.mapIndexed { index, type -> type.read("args[$index]") } + // A member's receiver is `args[0]` and is not one of [resolvedParams] (issue #215), so its + // declared parameters start at `args[1]`, as in KSP's `METHOD` entries. + val firstArgument = if (member != null) 1 else 0 + val argumentExpressions = resolvedParams.mapIndexed { index, type -> type.read("args[${index + firstArgument}]") } // Kotlin has no syntax to call an extension function by fully qualifying it the way an // ordinary top-level function or a Java static can be (`pkg.fn(args)`) -- the receiver can @@ -2013,7 +2313,9 @@ internal object ArtifactScanner { /** The call that passes everything, positionally -- byte for byte what this generator * emitted before defaults existed, and still the `else`-less first branch below. */ - val call = if (function.isExtension) { + val call = if (member != null) { + "${member.read}.${function.kotlinName}$typeArguments(${argumentExpressions.joinToString(", ")})" + } else if (function.isExtension) { "${argumentExpressions.first()}.$alias$typeArguments(${argumentExpressions.drop(1).joinToString(", ")})" } else { "$owner.${function.kotlinName}$typeArguments(${argumentExpressions.joinToString(", ")})" @@ -2030,10 +2332,11 @@ internal object ArtifactScanner { arity = arity, paramTags = resolvedParams.map { it.tag }, returnTag = returnType.tag, - lambdaBody = if (arity == 0) "{ ${returnType.wrapReturn(call)} }" + // A member reads its receiver out of `args[0]` even with no declared parameter. + lambdaBody = if (arity == 0 && member == null) "{ ${returnType.wrapReturn(call)} }" else "{ args -> ${returnType.wrapReturn(call)} }", imports = imports, - receiverTypeName = function.receiverType?.let { kotlinClassifierNameOf(it) }, + receiverTypeName = member?.owner ?: function.receiverType?.let { kotlinClassifierNameOf(it) }, paramNames = function.allParameterNames, paramTypeNames = paramTypeNames, returnTypeName = returnTypeName, @@ -2041,15 +2344,21 @@ internal object ArtifactScanner { // Deliberately all `false` until [applyDefaultOmission] says otherwise: this column is a // statement about **this body**, and this body passes everything. paramHasDefault = List(resolvedParams.size) { false }, + kind = if (member != null) METHOD_KIND else "FUNCTION", ) - val omittable = omittableParameterIndices(function, receiverIndex) + // A member's defaults are not omittable yet (issue #215): no plan, so Python passes every + // argument and the stub says so. + val omittable = if (member != null) emptySet() else omittableParameterIndices(function, receiverIndex) + val modelParameters = model.parameters.mapIndexed { index, parameter -> + functionSlots[index + receiverIndex]?.modelType?.let { parameter.copy(type = it) } ?: parameter + } return Candidate( callable = callable, declaration = model.copy( bindingName = qualifiedName, returnBoundaryTag = returnType.tag, - receiverBoundaryTag = if (function.isExtension) resolvedParams.first().tag else null, - parameters = model.parameters.mapIndexed { index, parameter -> + receiverBoundaryTag = if (function.isExtension) resolvedParams.first().tag else model.receiverBoundaryTag, + parameters = modelParameters.mapIndexed { index, parameter -> parameter.copy(boundaryTag = resolvedParams[index + receiverIndex].tag) }, ), diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanWorkAction.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanWorkAction.kt index 5674f73b4..f7012ed3e 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanWorkAction.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanWorkAction.kt @@ -51,15 +51,17 @@ internal abstract class KlibScanWorkAction : WorkAction + val excludePackages: ListProperty val resultFile: RegularFileProperty } override fun execute() { val klib = parameters.klibFile.get().asFile val includes = parameters.includePrefixes.get() + val excludes = parameters.excludePackages.get() val result = KlibScanResult( - callables = KlibScanner.scanKlib(klib, includes), - declarations = KlibScanner.scanKlibDeclarations(klib, includes), + callables = KlibScanner.scanKlib(klib, includes, excludes), + declarations = KlibScanner.scanKlibDeclarations(klib, includes, excludes), ) ObjectOutputStream(parameters.resultFile.get().asFile.outputStream()).use { it.writeObject(result) } } @@ -99,7 +101,7 @@ internal data class KlibScanResult( * list into the worker copied the collision in with it, and * `:generatePythonArtifactBindings` failed with the same * `NoSuchMethodError: KotlinLibraryImplKt.createKotlinLibrary$default` the isolation was introduced to - * fix. In this repository it happened to work: `ksp-fixtures` resolves the plugin from an *included + * fix. In this repository it happened to work: the `fixtures/` modules resolve the plugin from an *included * build*, which does get a scope of its own, so no in-repo fixture can see this. * * The worker classpath is therefore built from an explicit coordinate list @@ -138,6 +140,7 @@ private fun pluginCodeSource(): File { internal fun WorkerExecutor.scanKlibIsolated( klib: File, includes: List, + excludes: List, scratchDirectory: File, readerClasspath: Collection, ): KlibScanResult { @@ -153,6 +156,7 @@ internal fun WorkerExecutor.scanKlibIsolated( queue.submit(KlibScanWorkAction::class.java) { klibFile.set(klib) includePrefixes.set(includes) + excludePackages.set(excludes) this.resultFile.set(resultFile) } queue.await() diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanner.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanner.kt index 450c95f28..d12334e21 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanner.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanner.kt @@ -1,8 +1,13 @@ package python.multiplatform.gradle.artifact +import org.jetbrains.kotlin.library.abi.AbiClass +import org.jetbrains.kotlin.library.abi.AbiClassKind import org.jetbrains.kotlin.library.abi.AbiClassifierReference +import org.jetbrains.kotlin.library.abi.AbiDeclarationContainer import org.jetbrains.kotlin.library.abi.AbiCompoundName import org.jetbrains.kotlin.library.abi.AbiFunction +import org.jetbrains.kotlin.library.abi.AbiModality +import org.jetbrains.kotlin.library.abi.AbiProperty import org.jetbrains.kotlin.library.abi.AbiQualifiedName import org.jetbrains.kotlin.library.abi.AbiType import org.jetbrains.kotlin.library.abi.AbiTypeArgument @@ -36,7 +41,7 @@ import java.io.File * proved it and both attempts at aligning the version that failed before classloader isolation was * tried. [KlibScanWorkAction] is that isolation: every caller of this object goes through * [WorkerExecutor.scanKlibIsolated][python.multiplatform.gradle.artifact.scanKlibIsolated] rather than - * calling [scanKlib]/[scanKlibDeclarations] directly, and `:ksp-fixtures:klib-artifact + * calling [scanKlib]/[scanKlibDeclarations] directly, and `:python-multiplatform-gradle-plugin-fixtures:klib-artifact * :generatePythonArtifactBindings`/`:generatePythonStubs` passing is what confirms the isolation * actually holds in a consumer build, not only in this file's own unit tests. * @@ -163,36 +168,163 @@ internal object KlibScanner { */ private val PUBLISHED_API = AbiQualifiedName(AbiCompoundName("kotlin"), AbiCompoundName("PublishedApi")) + private val DEPRECATED = AbiQualifiedName(AbiCompoundName("kotlin"), AbiCompoundName("Deprecated")) + /** Every binding this klib offers, sorted by name -- the counterpart of [ArtifactScanner.scanJar]. */ - fun scanKlib(klib: File, includePrefixes: List): List = - scan(klib, includePrefixes).mapNotNull { it.callable }.sortedBy { it.name } + fun scanKlib(klib: File, includePrefixes: List, excludePackages: List = emptyList()): List = + scan(klib, includePrefixes, excludePackages).mapNotNull { it.callable }.sortedBy { it.name } /** The same walk with the other half of its result kept -- the counterpart of * [ArtifactScanner.scanDeclarations], sorted the same way so the two producers' models can be * concatenated without either one's order depending on which walked first. */ - fun scanKlibDeclarations(klib: File, includePrefixes: List): List = - scan(klib, includePrefixes) + fun scanKlibDeclarations(klib: File, includePrefixes: List, excludePackages: List = emptyList()): List = + scan(klib, includePrefixes, excludePackages) .map { it.declaration } .sortedWith(compareBy({ it.owner }, { it.simpleName }, { it.bindingName ?: "" })) - private fun scan(klib: File, includePrefixes: List): List { + private fun scan(klib: File, includePrefixes: List, excludePackages: List): List { val abi = try { LibraryAbiReader.readAbiInfo(klib) } catch (e: Exception) { return emptyList() } + val classes = classesOf(abi.topLevelDeclarations) val candidates = abi.topLevelDeclarations.declarations .filterIsInstance() .filter { matchesInclude(it.qualifiedName, includePrefixes) } + // SPEC B-11: `internal` packages and `excludePackages`, as on the JVM producer. + .filter { !ArtifactScanner.isExcludedPackage(it.qualifiedName.packageName.toString(), excludePackages) } // Not a *top-level* shape at all: a constructor belongs to an `AbiClass` and needs a // receiver handle to be worth anything. Filtered rather than declined for the same reason // `ArtifactScanner` never builds a model for ``. .filter { !it.isConstructor } - .mapNotNull { candidateOrNull(it) } + .mapNotNull { candidateOrNull(it, classes) } + + // Issue #237: the member functions of the klib's classes, as #215 binds them from a jar. + val members = classes.values + .filter { matchesInclude(it.qualifiedName, includePrefixes) } + .filter { !ArtifactScanner.isExcludedPackage(it.qualifiedName.packageName.toString(), excludePackages) } + .flatMap { memberCandidatesOf(it, classes) } - // Same rule, same implementation as the JVM producer -- see this object's KDoc. - return ArtifactScanner.disambiguateOverloads(candidates) + // Same rule, same implementation as the JVM producer -- see this object's KDoc. A member's key + // is `Owner.name`, so its overload set is its class's alone. + return ArtifactScanner.disambiguateOverloads(candidates + members) + } + + /** + * The member functions of [cls] (issue #237), as `METHOD` entries `Owner.name` -- the shape #215 + * gives a jar's members and KSP gives its own: `args[0]` is the receiver, not counted in `arity`, + * and the generated call is `(args[0] as Owner).name(...)`. The binding layer serves them as + * methods of the receiver's proxy, and never as a module attribute. + * + * The class must be able to cross as a handle ([handleClassOrNull]); a generic one's members are + * declined, as on the jar side. A member is declined, with its reason, when it is `suspend`, + * `@Deprecated`, `@PublishedApi`, `@Composable`, a member extension or one with context receivers, + * named like a property of its class (the property's name is the attribute), or typed by something + * without a klib boundary type. Constructors are not read here (stage 3). + */ + private fun memberCandidatesOf(cls: AbiClass, classes: Map): List { + val owner = cls.qualifiedName.dotted() + val ownerType = KotlinTypeModel(owner) + val generic = cls.typeParameters.isNotEmpty() + if (!generic && cls.kind != AbiClassKind.OBJECT && handleClassOrNull(ownerType, classes) == null) return emptyList() + val propertyNames = cls.declarations.filterIsInstance() + .mapTo(HashSet()) { it.qualifiedName.relativeName.toString().substringAfterLast('.') } + // The pinned compiler's klib ABI records modality but no visibility, and lists the `protected` + // members of an open, abstract or sealed class beside its public ones; source outside a subclass + // cannot call a `protected` one. An interface has none, and a final class's are effectively + // private, so only those members bind. An `object`'s functions are statics on the jar side (#53), + // called without a receiver, which this `METHOD` shape is not. + val unbindable = when { + cls.kind == AbiClassKind.OBJECT -> "a function of an object: bound as a static on the jar side (#53), not yet from a klib" + cls.kind == AbiClassKind.CLASS && cls.modality != AbiModality.FINAL -> + "a member of a non-final class: this klib ABI does not say whether it is protected" + else -> null + } + val enum = cls.kind == AbiClassKind.ENUM_CLASS + return cls.declarations.filterIsInstance() + .filter { !it.isConstructor } + // `Any`'s members are not the class's API, as on the jar side; an enum class's + // `valueOf`/`values`/`entries` are statics the ABI lists among its members, and no + // instance call reaches them. + .filter { function -> !isAnyMember(function) && !(enum && function.leafName() in ENUM_STATICS) } + .mapNotNull { function -> memberCandidateOrNull(function, owner, ownerType, generic, unbindable, propertyNames, classes) } + } + + private fun memberCandidateOrNull( + function: AbiFunction, + owner: String, + ownerType: KotlinTypeModel, + generic: Boolean, + unbindable: String?, + propertyNames: Set, + classes: Map, + ): Candidate? { + val name = function.leafName() + val model = declarationModelOf(function)?.copy( + simpleName = name, + owner = owner, + ownerIsClass = true, + receiver = ownerType, + receiverBoundaryTag = OBJECT_TAG, + kind = METHOD_KIND, + ) ?: return null + val declined = { reason: String -> Candidate(null, model.copy(declineReason = reason)) } + when { + function.isSuspend -> return declined("suspend") + generic -> return declined("a member of a generic class: not bound yet") + unbindable != null -> return declined(unbindable) + function.hasAnnotation(DEPRECATED) -> + return declined("@Deprecated: its level is not in this klib ABI, and a HIDDEN one cannot be called from source") + function.hasAnnotation(PUBLISHED_API) -> + return declined("@PublishedApi internal -- binary-visible for inlining, not part of the module's public Kotlin API") + function.hasAnnotation(COMPOSABLE) -> return declined("a @Composable member function: not bound on a proxy") + function.hasExtensionReceiverParameter -> return declined("a member extension: it needs two receivers") + function.contextReceiverParametersCount > 0 -> + return declined("${function.contextReceiverParametersCount} context receiver(s), whose types this klib ABI does not expose") + name in propertyNames -> return declined("shares its name with a property of the class, which keeps the attribute") + // A data class's `copy`/`componentN` are synthetics the jar side never binds (U-4), and this + // ABI cannot tell them from a hand-written member of the same name. + name == "copy" || DATA_COMPONENT.matches(name) -> + return declined("a data-class synthetic (copy/componentN), or a member this klib ABI cannot tell from one") + } + val returnType = klibBoundaryTypeOf(model.returnType, classes) + ?: return declined("no boundary type for return ${model.returnType.render()}") + val boundaryTypes = model.parameters.map { parameter -> + klibBoundaryTypeOf(parameter.type, classes) + ?: return declined("no boundary type for parameter ${parameter.type.render()}") + } + if (boundaryTypes.any { it.isReturnOnly }) return declined("a parameter of a return-only boundary type") + + val key = "$owner.$name" + val argumentExpressions = boundaryTypes.mapIndexed { index, type -> type.read("args[${index + 1}]") } + val body = returnType.wrapReturn("(args[0] as $owner).$name(${argumentExpressions.joinToString(", ")})") + val callable = ArtifactCallable( + name = key, + arity = boundaryTypes.size, + paramTags = boundaryTypes.map { it.tag }, + returnTag = returnType.tag, + // The receiver is read even with no declared parameter, so the body always takes `args`. + lambdaBody = "{ args -> $body }", + receiverTypeName = owner, + paramTypeNames = model.parameters.map { it.type.qualifiedName }, + returnTypeName = model.returnType.qualifiedName, + returnSupertypes = if (returnType.tag == OBJECT_TAG) supertypesOf(model.returnType.qualifiedName, classes) else emptyList(), + // Every argument is passed, as for a top-level klib entry (see [candidateOrNull]). + paramHasDefault = List(boundaryTypes.size) { false }, + kind = METHOD_KIND, + ) + return Candidate( + callable = callable, + declaration = model.copy( + bindingName = key, + returnBoundaryTag = returnType.tag, + parameters = model.parameters.mapIndexed { index, parameter -> + parameter.copy(boundaryTag = boundaryTypes[index].tag, declaresDefault = false) + }, + ), + ) } /** @@ -232,7 +364,27 @@ internal object KlibScanner { * `scanKlibDeclarations` only ever call `LibraryAbiReader.readAbiInfo` on a real file and have no * seam to inject one. */ - internal fun candidateOrNull(function: AbiFunction): Candidate? { + /** + * Every class the klib declares, nested ones included, by its dotted Kotlin name -- what + * [klibBoundaryTypeOf] asks before it lets a type cross as a handle (issue #234). + */ + private fun classesOf(container: AbiDeclarationContainer): Map { + val found = LinkedHashMap() + fun walk(current: AbiDeclarationContainer) { + current.declarations.filterIsInstance().forEach { cls -> + found[cls.qualifiedName.dotted()] = cls + walk(cls) + } + } + walk(container) + return found + } + + /** + * @param classes the walked klib's own classes ([classesOf]): a parameter or result whose class + * is one of them, and qualifies ([handleClassOrNull]), crosses as an object handle. + */ + internal fun candidateOrNull(function: AbiFunction, classes: Map = emptyMap()): Candidate? { val model = declarationModelOf(function) ?: return null val declined = { reason: String -> Candidate(null, model.copy(declineReason = reason)) } @@ -243,6 +395,12 @@ internal object KlibScanner { if (function.isSuspend) return declined("suspend") // See PUBLISHED_API's KDoc: `@PublishedApi internal` is binary-visible but not // source-visible, and the generated call expression is source in the consumer's own module. + // A `@Deprecated(level = HIDDEN)` declaration stays in a klib for binary compatibility + // (`kotlinx.coroutines.Job0`) but cannot be called from source, and this ABI exposes the + // annotation but not its level (issue #234), so every deprecated declaration is declined. + if (function.hasAnnotation(DEPRECATED)) { + return declined("@Deprecated: its level is not in this klib ABI, and a HIDDEN one cannot be called from source") + } if (function.hasAnnotation(PUBLISHED_API)) { return declined("@PublishedApi internal -- binary-visible for inlining, not part of the module's public Kotlin API") } @@ -259,10 +417,10 @@ internal object KlibScanner { return declined("${function.contextReceiverParametersCount} context receiver(s), whose types this klib ABI does not expose") } - val returnType = klibBoundaryTypeOf(model.returnType) + val returnType = klibBoundaryTypeOf(model.returnType, classes) ?: return declined("no boundary type for return ${model.returnType.render()}") val boundaryTypes = model.parameters.map { parameter -> - klibBoundaryTypeOf(parameter.type) + klibBoundaryTypeOf(parameter.type, classes) ?: return declined("no boundary type for parameter ${parameter.type.render()}") } if (boundaryTypes.any { it.isReturnOnly }) { @@ -286,6 +444,10 @@ internal object KlibScanner { // supplied"; `DeclarationModel.parameterNamesKnown` says the same thing to the stub side. paramTypeNames = model.parameters.map { it.type.qualifiedName }, returnTypeName = model.returnType.qualifiedName, + // What a handle result **is a** (issue #234), read off the klib's own supertypes, nearest + // first: `_member` and `_is_a` read it, so a `CompletableJob` serves `Job`'s members and fills + // a `Job` slot. Empty for a non-handle result, as on the jar side. + returnSupertypes = if (returnType.tag == OBJECT_TAG) supertypesOf(model.returnType.qualifiedName, classes) else emptyList(), // Deliberately all `false`, not `model.parameters.map { it.declaresDefault }`. // `ArtifactCallable.paramHasDefault`'s contract (see its KDoc) is a statement about // [lambdaBody] -- exactly the slots the generated call may omit -- and never about the @@ -403,9 +565,21 @@ internal object KlibScanner { * an object handle in that case; this walker has no object boundary at all yet, so it declines. * Type *arguments* are ignored rather than refused, because no type this admits has any. */ - private fun klibBoundaryTypeOf(type: KotlinTypeModel): BoundaryType? { - if (type.isNullable) return null - return when (type.qualifiedName) { + private fun klibBoundaryTypeOf(type: KotlinTypeModel, classes: Map = emptyMap()): BoundaryType? { + // Issue #234: a class of the walked klib crosses as an object handle -- the jar producer's + // `(%s as Type)` read and an unwrapped result, which the common trampoline turns into a + // `HandleTable` handle and back. A nullable one carries the `?`: the handle is already + // nullable at the boundary (`UpcallTrampoline.toKotlin` maps Python `None` to `null`). + if (!type.isNullable) primitiveBoundaryTypeOf(type.qualifiedName)?.let { return it } + if (handleClassOrNull(type, classes) != null) { + val cast = type.qualifiedName + if (type.isNullable) "?" else "" + return BoundaryType(OBJECT_TAG, "(%s as $cast)", "(%s)") + } + return null + } + + private fun primitiveBoundaryTypeOf(qualifiedName: String): BoundaryType? { + return when (qualifiedName) { "kotlin.Boolean" -> BoundaryType("BOOLEAN", "(%s as Boolean)", "(%s)") "kotlin.Byte" -> BoundaryType("INT", "(%s as Long).toByte()", "(%s).toLong()") "kotlin.Short" -> BoundaryType("INT", "(%s as Long).toShort()", "(%s).toLong()") @@ -419,4 +593,58 @@ internal object KlibScanner { else -> null } } + + private const val OBJECT_TAG = "OBJECT" + + private const val METHOD_KIND = "METHOD" + + private val ENUM_STATICS = setOf("valueOf", "values", "entries") + + private val DATA_COMPONENT = Regex("component[1-9][0-9]*") + + private fun AbiFunction.leafName(): String = qualifiedName.relativeName.toString().substringAfterLast('.') + + /** `toString()`, `hashCode()` and `equals(other)`: what every class has from `Any`. */ + private fun isAnyMember(function: AbiFunction): Boolean = when (function.leafName()) { + "toString", "hashCode" -> function.valueParameters.isEmpty() + "equals" -> function.valueParameters.size == 1 + else -> false + } + + /** + * The walked klib's class [type] names, when a handle of it can cross (issue #234): a public + * (the ABI lists nothing else) `class`, `interface`, `enum class` or `object` that is not a value + * class -- a value class's identity is not readable here (this object's KDoc) -- has no type + * parameters, is used without type arguments, and is not `@PublishedApi internal`. Anything else, + * a type from another klib included, is `null`, and the declaration stays declined. + */ + private fun handleClassOrNull(type: KotlinTypeModel, classes: Map): AbiClass? { + if (type.arguments.isNotEmpty()) return null + // `kotlin.Int`, `kotlin.String` and the rest of package `kotlin` are classes of the stdlib klib, + // and the boundary's own currency, never handles. + if (type.qualifiedName.substringBeforeLast('.') == "kotlin") return null + val cls = classes[type.qualifiedName] ?: return null + if (cls.kind !in HANDLE_CLASS_KINDS || cls.isValue || cls.typeParameters.isNotEmpty()) return null + if (cls.hasAnnotation(PUBLISHED_API)) return null + return cls + } + + private val HANDLE_CLASS_KINDS = setOf(AbiClassKind.CLASS, AbiClassKind.INTERFACE, AbiClassKind.ENUM_CLASS, AbiClassKind.OBJECT) + + /** [qualifiedName]'s supertypes, nearest first and transitive within the klib, `kotlin.Any` left out. */ + private fun supertypesOf(qualifiedName: String, classes: Map): List { + val ordered = LinkedHashSet() + var frontier = listOf(qualifiedName) + repeat(16) { + val next = frontier.flatMap { name -> + classes[name]?.superTypes.orEmpty().mapNotNull { superType -> + ((superType as? AbiType.Simple)?.classifierReference as? AbiClassifierReference.ClassReference) + ?.className?.dotted() + } + }.filter { it != "kotlin.Any" && it != qualifiedName && ordered.add(it) } + if (next.isEmpty()) return ordered.toList() + frontier = next + } + return ordered.toList() + } } diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KotlinMetadata.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KotlinMetadata.kt index 961fcf371..ee559b060 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KotlinMetadata.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KotlinMetadata.kt @@ -273,7 +273,12 @@ internal fun resolveKotlinBoundary( if (info != null) { val underlying = resolveKotlinBoundary(info.underlyingType, classpath, direction) val usable = when (direction) { - BoundaryDirection.PARAMETER -> info.constructorIsPublic + // Issue #168: opened as a raw primitive only when it can be opened **both ways**. A value + // class whose property is not public (`TextUnitType`) reaches Python as a boxed handle + // when Kotlin returns one (`TextUnitType.Sp`), so a parameter of it must take that handle. + // Marshalled as the raw primitive it would read a number and refuse the very value Kotlin + // handed out; as an object it is cast (`args[i] as TextUnitType`) and kotlinc unboxes. + BoundaryDirection.PARAMETER -> info.constructorIsPublic && info.propertyIsPublic && info.propertyName != null BoundaryDirection.RETURN -> info.propertyIsPublic && info.propertyName != null } // Falls through to the object handle below when the wrapper cannot be opened from outside diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/PythonArtifactBindingsTask.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/PythonArtifactBindingsTask.kt index abf9edf50..f7b8936e1 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/PythonArtifactBindingsTask.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/PythonArtifactBindingsTask.kt @@ -1,5 +1,6 @@ package python.multiplatform.gradle.artifact +import python.multiplatform.gradle.deleteWithoutFollowingLinks import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty @@ -61,6 +62,10 @@ abstract class PythonArtifactBindingsTask : DefaultTask() { @get:Input abstract val includePrefixes: ListProperty + /** Packages to leave out (`pythonBindings.excludePackages`), on top of every `internal` package; SPEC B-11. */ + @get:Input + abstract val excludePackages: ListProperty + /** * The whole classpath of the isolated worker that runs [KlibScanner], bar this plugin's own jar: * the klib reader plus what [ArtifactScanner] needs, resolved by the consumer's project. See @@ -97,14 +102,15 @@ abstract class PythonArtifactBindingsTask : DefaultTask() { val destination = outputDirectory.get().asFile.resolve(ARTIFACTS_PACKAGE.replace('.', '/')) // Rebuilt from scratch: a dependency that was removed, or an include prefix that stopped // matching, must take its fragment file with it or the next compilation still sees it. - outputDirectory.get().asFile.deleteRecursively() + deleteWithoutFollowingLinks(outputDirectory.get().asFile) destination.mkdirs() val thunkRoot = thunkDirectory.get().asFile - thunkRoot.deleteRecursively() + deleteWithoutFollowingLinks(thunkRoot) val thunkDestination = thunkRoot.resolve(THUNK_PACKAGE.replace('.', '/')) thunkDestination.mkdirs() val includes = includePrefixes.get() + val excludes = excludePackages.get() val coordinates = coordinatesByFileName.get() val objectNames = mutableListOf() @@ -119,7 +125,7 @@ abstract class PythonArtifactBindingsTask : DefaultTask() { .sortedBy { it.name } .forEach { file -> val entries = if (file.name.endsWith(".jar")) { - ArtifactScanner.scanJar(file, includes, classpath = resolvedJars) + ArtifactScanner.scanJar(file, includes, classpath = resolvedJars, excludePackages = excludes) } else { // The klib path takes no classpath yet, and whether it needs one is open // rather than overlooked: the jar path needs other artefacts because an @@ -128,7 +134,7 @@ abstract class PythonArtifactBindingsTask : DefaultTask() { // // Not a direct `KlibScanner.scanKlib` call: see this class's KDoc, "The klib // branch runs behind a worker classloader boundary". - workerExecutor.scanKlibIsolated(file, includes, temporaryDir, klibReaderClasspath.files).callables + workerExecutor.scanKlibIsolated(file, includes, excludes, temporaryDir, klibReaderClasspath.files).callables } // An artefact that contributed nothing gets no fragment: a fragment with no entries // would still take a `moduleName` in `UpcallTable`'s installed set, which is the one diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt index 4cea7f641..93082c4a1 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt @@ -10,8 +10,27 @@ import python.multiplatform.gradle.model.ValueClassModel * * One `def` per bound declaration, in the module the runtime publishes it onto, under the Kotlin * names. The binder never exports a Kotlin namespace under another name (AGENTS.md §12 rule 1), so - * there is no second, renamed product: a `pythonx` module is a real Python package that imports - * these modules and makes them Pythonic, and its own stubs are its own. + * there is no second, renamed product and nothing under `pythonx`: a `pythonx` module is a real + * Python package that imports these modules, and its own stubs are its own. + * + * ### Pythonic names (issue #131) + * + * The runtime serves every lower-case-first declaration and member under its snake_case alias too, + * and takes each keyword by its Kotlin name or its snake_case one ([pythonName], [snakeCase], + * [pythonicAliases]; the runtime's rule in `KotlinSurface.kt`). The stub says the same: + * + * - **parameters** are written under the keyword `inspect.signature` shows -- the snake_case alias + * where one is served for that declaration, else the Kotlin name. A stub can name a parameter only + * once, and it names it the way the signature does; a Kotlin keyword still works at run time, but a + * checker reports it; + * - **module-level names** keep their Kotlin `def` (or constant), followed by `alias = kotlinName` for + * each alias the runtime serves in that module, so the alias has exactly the Kotlin name's type; + * - **members of a receiver's class** get the alias as a second `ClassVar` of the same `Protocol`, or a + * second `@property` with the same table key in its docstring; + * - members of a callable module (#78) get the alias as a second member. + * + * Not aliased: a class module folded into its parent for a case-insensitive filesystem (#44) -- its + * functions keep their Kotlin names only in the stub, although the runtime serves the alias. * * Returns *relative path to file content*, so nothing here touches a disk and every assertion in * `PyiRenderingTest` and `TypedStubTest` is over text. `PythonStubsTask` is the only thing that writes. @@ -95,7 +114,15 @@ internal const val PROPERTY_MARKER = "Kotlin property: " */ internal const val CONSTRUCTOR_MARKER = "Kotlin constructor: " +/** + * What every member-function stub (issue #215) carries after its table key, in the docstring of the + * method or in the comment that says why there is none -- the one place a test can match a key to its + * stub, as [PROPERTY_MARKER] is for a property. + */ +internal const val METHOD_MARKER = "Kotlin member: " + private val PROPERTY_KINDS = setOf("GETTER", "SETTER") +private const val METHOD_KIND = "METHOD" /** Python's hard keywords (`keyword.kwlist`): none of these can be a parameter name. */ private val PYTHON_KEYWORDS = setOf( @@ -173,8 +200,13 @@ private enum class Role { PARAM, RESULT } private class StubRenderer(declarations: List) { - /** Everything that is a module attribute; properties are attributes of a proxy instead. */ - private val bound: List = declarations.filter { it.kind !in PROPERTY_KINDS } + /** Everything that is a module attribute; properties and member functions are attributes of a proxy instead. */ + private val bound: List = declarations.filter { it.kind !in PROPERTY_KINDS && it.kind != METHOD_KIND } + + /** receiver type -> its bound member functions (issue #215), each a method of the receiver's stub class. */ + private val methods: Map> = declarations + .filter { it.kind == METHOD_KIND && it.bindingName != null && it.receiver != null } + .groupBy { it.receiver!!.qualifiedName } /** receiver type -> property name -> (getter, setter). A setter never stands without its getter. */ private val properties: Map>> = run { @@ -337,6 +369,21 @@ private class StubRenderer(declarations: List) { * (a module and a class cannot share the name, which is why this used to fall back to `Any`). * The object's own type (`Alignment`) has no such home and stays unresolved here. */ + /** + * Where [ref]'s class is declared in its module's stub: its own path, or -- when a function of the + * module takes its top name (Kotlin's factory idiom, `Job` and `fun Job()`, issue #217) -- the same + * path under the stub-private name `_`. The function keeps the name, as at run time, and the + * type keeps a class every use of it can name; nothing at run time is called `_`. `null` when + * even that name is taken. + */ + private fun stubPathOf(ref: ClassRef): List? { + val taken = takenNames[ref.pkg].orEmpty() + if (ref.top !in taken) return ref.path + val private = "_" + ref.top + if (private in taken) return null + return listOf(private) + ref.path.drop(1) + } + private fun resolveRef(qualifiedName: String): ClassRef? { val ref = classRefOf(qualifiedName) ?: return null val objectModule = "${ref.pkg}.${ref.top}" @@ -346,9 +393,10 @@ private class StubRenderer(declarations: List) { private fun classReference(qualifiedName: String, valueClass: ValueClassModel?, ctx: ModuleOut): String { val ref = resolveRef(qualifiedName) ?: return ANY - if (ref.top in takenNames[ref.pkg].orEmpty() || "${ref.pkg}.${ref.top}" in moduleNames) return ANY + if ("${ref.pkg}.${ref.top}" in moduleNames) return ANY + val stubPath = stubPathOf(ref) ?: return ANY if (qualifiedName !in classes || classes[qualifiedName] == null) classes[qualifiedName] = valueClass ?: classes[qualifiedName] - val path = ref.path.joinToString(".") + val path = stubPath.joinToString(".") if (ref.pkg == ctx.module) return path ctx.imports += ref.pkg return "${ref.pkg}.$path" @@ -364,7 +412,11 @@ private class StubRenderer(declarations: List) { hasReceiver -> RECEIVER_NAME else -> null } - val writable = parameters.map { d.parameterNamesKnown && isWritableName(it.name, reserved) } + // Issue #131: each parameter under the keyword `inspect.signature` shows at run time -- its + // snake_case alias where one is served for this declaration, else its Kotlin name. The runtime + // accepts both spellings; a stub can name only one. + val shown = pythonicParameterNames(d) + val writable = parameters.indices.map { d.parameterNamesKnown && isWritableName(shown[it], reserved) } // Everything up to and including the last unwritable name is positional-only. val positionalOnlyEnd = writable.indexOfLast { !it } val lastRequired = parameters.indexOfLast { !it.declaresDefault } @@ -383,7 +435,7 @@ private class StubRenderer(declarations: List) { } parameters.forEachIndexed { index, parameter -> if (index == keywordOnlyStart) rendered += "*" - val name = if (writable[index]) parameter.name!! else "__a$index" + val name = if (writable[index]) shown[index]!! else "__a$index" val marked = if (positionalOnlyEnd >= 0) parameter.declaresDefault && index > lastRequired else parameter.declaresDefault rendered += "$name: " + annotate(parameter.type, parameter.boundaryTag, Role.PARAM, ctx) + if (marked) " = ..." else "" if (index == positionalOnlyEnd) rendered += "/" @@ -392,6 +444,20 @@ private class StubRenderer(declarations: List) { return rendered.joinToString(", ") } + /** + * Per parameter, the name `KotlinSurface.signature_of` shows: the snake_case alias the runtime + * serves for it (`keyword_slots`: not another parameter's Kotlin name, not shared by two + * parameters, not a Python keyword), else the Kotlin name; `null` where the name is unknown. + */ + private fun pythonicParameterNames(d: DeclarationModel): List { + val kotlinNames = d.parameters.mapNotNull { it.name } + val aliasOf = pythonicAliases(kotlinNames, ::snakeCase).entries.associate { (alias, kotlin) -> kotlin to alias } + return d.parameters.map { parameter -> + val name = parameter.name ?: return@map null + aliasOf[name]?.takeIf { it !in PYTHON_KEYWORDS } ?: name + } + } + private fun result(d: DeclarationModel, ctx: ModuleOut): String = annotate(d.returnType, d.returnBoundaryTag, Role.RESULT, ctx) @@ -448,6 +514,9 @@ private class StubRenderer(declarations: List) { attributes.getOrPut(leaf) { mutableListOf() } += d if (d.kind != "STATIC_GETTER" && isSuffixed(leaf)) attributes.getOrPut(baseOf(leaf)) { mutableListOf() } += d } + // Issue #131: the module's Pythonic aliases are members too, typed as their Kotlin names. + val namespace = attributes.keys + childSegments("${ctx.module}.$name") + pythonicAliases(namespace).forEach { (alias, kotlin) -> attributes[kotlin]?.let { attributes[alias] = it } } attributes.forEach { (attribute, group) -> if (!isIdentifier(attribute) || attribute == "__call__") { appendLine(" # '$attribute' cannot be written as an attribute here; reach it with getattr(..., '$attribute')") @@ -514,15 +583,35 @@ private class StubRenderer(declarations: List) { } // The base name of an overload set: the binding layer serves it, so the stub says it. val unsuffixedLeaves = sorted.filter { !isSuffixed(leafOf(it)) }.map { leafOf(it) }.toSet() + val overloadBases = mutableSetOf() sorted.filter { it.kind != "STATIC_GETTER" && isSuffixed(leafOf(it)) } .groupBy { baseOf(leafOf(it)) } .toSortedMap() .forEach { (base, group) -> - if (base !in unsuffixedLeaves && base !in companions) ctx.defs += overloadDefs(base, group, ctx) + if (base !in unsuffixedLeaves && base !in companions) { + ctx.defs += overloadDefs(base, group, ctx) + overloadBases += base + } } + // Issue #131: the Pythonic alias of every name written above, as an assignment, so a + // checker gives it exactly the Kotlin name's type. The namespace is the runtime's + // (`_package_aliases`): every bound name of the package and its child packages. + val written = sorted.map { leafOf(it) } + constructors.filter { isSuffixed(leafOf(it)) }.map { leafOf(it) } + + companions + overloadBases + val namespace = entries.flatMap { listOf(leafOf(it), baseOf(leafOf(it))) } + childSegments(module) + ctx.defs += aliasAssignments(pythonicAliases(namespace), written.toSet()) } } + /** The segment directly under [module] of every module below it: its child packages and objects. */ + private fun childSegments(module: String): List = + byModule.keys.filter { it.startsWith("$module.") }.map { it.removePrefix("$module.").substringBefore('.') }.distinct() + + /** `alias = kotlinName` for each alias whose Kotlin name the stub module writes, sorted by alias. */ + private fun aliasAssignments(aliases: Map, written: Set): List = + aliases.filter { (alias, kotlin) -> kotlin in written && kotlin !in PYTHON_KEYWORDS && alias !in PYTHON_KEYWORDS } + .map { (alias, kotlin) -> "$alias = $kotlin" } + /** * A class module that collides case-insensitively with a sibling path (issue #44): its functions * become static members of the class of that name in the parent package's module, nested for the @@ -587,14 +676,21 @@ private class StubRenderer(declarations: List) { private fun renderClass(qualifiedName: String) { val ref = resolveRef(qualifiedName) ?: return val ctx = out(ref.pkg) - val node = ctx.node(ref.path) + val stubPath = stubPathOf(ref) ?: return + val node = ctx.node(stubPath) node.qualifiedName = qualifiedName val valueClass = classes[qualifiedName] + // A class under its stub-private name (issue #217) says so, and why, whatever else it says. + val privateNote = if (stubPath.first() != ref.top) { + " (stubbed as ${stubPath.joinToString(".")}: '${ref.top}' is the function of that name in its module)" + } else { + "" + } node.doc = if (valueClass != null) { - "Kotlin value class $qualifiedName over ${valueClass.underlying.qualifiedName}. As a parameter the raw " + + "Kotlin value class $qualifiedName$privateNote over ${valueClass.underlying.qualifiedName}. As a parameter the raw " + "primitive is also accepted (the binding layer's allowlist decides, at run time); a result is the raw primitive." } else { - "Kotlin: $qualifiedName" + "Kotlin: $qualifiedName$privateNote" } stubBasesOf(qualifiedName, ctx).takeIf { it.isNotEmpty() }?.let { node.bases = it.joinToString(", ") } @@ -610,12 +706,25 @@ private class StubRenderer(declarations: List) { } } + // Issue #215: member functions, by explicit name and, for an overload set, by base name -- the two + // keys `_index` serves them under. A member wins over an extension of the same name, as at run time. + val memberFunctions = sortedMapOf>() + methods[qualifiedName].orEmpty().sortedBy { it.bindingName }.forEach { d -> + val leaf = leafOf(d) + memberFunctions.getOrPut(leaf) { mutableListOf() } += d + if (isSuffixed(leaf)) memberFunctions.getOrPut(baseOf(leaf)) { mutableListOf() } += d + } val attributes = sortedMapOf>() extensionsByReceiver[qualifiedName].orEmpty().sortedBy { it.bindingName }.forEach { d -> val leaf = leafOf(d) + if (leaf in memberFunctions) return@forEach attributes.getOrPut(leaf) { mutableListOf() } += d - if (isSuffixed(leaf)) attributes.getOrPut(baseOf(leaf)) { mutableListOf() } += d + if (isSuffixed(leaf) && baseOf(leaf) !in memberFunctions) attributes.getOrPut(baseOf(leaf)) { mutableListOf() } += d } + // Issue #131: the proxy also serves each member's Pythonic alias, decided over every member + // name the runtime looks up for this type -- its own and every supertype's (`_member_aliases`). + val aliases = pythonicAliases(memberNamesOf(qualifiedName)) + val protocols = mutableMapOf() attributes.forEach { (attribute, group) -> if (!isIdentifier(attribute) || attribute in node.children) return@forEach val protocol = "_" + ref.path.joinToString("_") + "_" + attribute @@ -628,7 +737,19 @@ private class StubRenderer(declarations: List) { } } node.members += "$attribute: $TYPING.ClassVar[$protocol]" + protocols[attribute] = protocol + } + val stubbedMethods = mutableMapOf() + memberFunctions.forEach { (name, group) -> + if (!isIdentifier(name) || name in node.children) { + group.filter { leafOf(it) == name }.forEach { node.members += methodNotStubbed(it, "'$name' cannot be written as an attribute here") } + return@forEach + } + val rendered = renderMethods(name, group, ctx) + node.members += rendered + stubbedMethods[name] = name } + val stubbedProperties = mutableMapOf>() properties[qualifiedName].orEmpty().forEach { (name, accessors) -> val (getter, setter) = accessors if (!isIdentifier(name) || name in node.children || name in attributes) { @@ -636,9 +757,43 @@ private class StubRenderer(declarations: List) { return@forEach } node.members += renderProperty(name, getter, setter, ctx) + stubbedProperties[name] = accessors + } + aliases.forEach { (alias, kotlin) -> + if (!isIdentifier(alias) || alias in node.children) return@forEach + protocols[kotlin]?.let { node.members += "$alias: $TYPING.ClassVar[$it]" } + stubbedProperties[kotlin]?.let { (getter, setter) -> node.members += renderProperty(alias, getter, setter, ctx) } + if (kotlin in stubbedMethods && alias !in memberFunctions) node.members += renderMethods(alias, memberFunctions.getValue(kotlin), ctx) } } + /** + * One member function, or an overload set under its base name, as methods named [name]. A single + * declaration's method carries its table key after [METHOD_MARKER]; an overload set's `@overload`s + * do not, because each member is also stubbed under its own explicit name, which does. + */ + private fun renderMethods(name: String, group: List, ctx: ModuleOut): String = + if (group.size == 1) { + val d = group.single() + "def $name(${parameters(d, ctx, method = true)}) -> ${result(d, ctx)}:\n" + + " \"\"\"$METHOD_MARKER${d.bindingName}\"\"\"\n" + + " ..." + } else { + group.mapIndexed { index, d -> + "@$TYPING.overload\ndef $name(${parameters(d, ctx, method = true)}) -> ${result(d, ctx)}: ..." + shadowed(index) + }.joinToString("\n") + } + + private fun methodNotStubbed(d: DeclarationModel, why: String): String = "# $METHOD_MARKER${d.bindingName} is not stubbed: $why" + + /** Every member name the binding layer serves on a proxy of [qualifiedName]: `_member_names`. */ + private fun memberNamesOf(qualifiedName: String): Set = + (listOf(qualifiedName) + supertypes[qualifiedName].orEmpty()).flatMap { type -> + extensionsByReceiver[type].orEmpty().flatMap { listOf(leafOf(it), baseOf(leafOf(it))) } + + methods[type].orEmpty().flatMap { listOf(leafOf(it), baseOf(leafOf(it))) } + + properties[type].orEmpty().keys + }.toSet() + private fun renderProperty(name: String, getter: DeclarationModel, setter: DeclarationModel?, ctx: ModuleOut): String = buildString { appendLine("@property") appendLine("def $name(self) -> ${result(getter, ctx)}:") @@ -667,6 +822,21 @@ private class StubRenderer(declarations: List) { * a value of it can still reach Python (a `kotlin.Any?` result, a supertype's slot). One whose * class cannot be stubbed gets the marker comment instead; see this file's KDoc. */ + /** + * Every receiver that has member functions gets its stub class, as one with properties does; one + * whose class cannot be stubbed gets a marker comment per member in its package's module instead. + */ + private fun registerMethodOwners() { + methods.forEach { (receiver, declarations) -> + val ref = classRefOf(receiver) + val ctx = out(ref?.pkg ?: receiver.substringBeforeLast('.', "")) + if (ref != null && classReference(receiver, null, ctx) != ANY) return@forEach + declarations.forEach { d -> + ctx.defs += methodNotStubbed(d, "${receiver.substringAfterLast('.')} has no stub class (its name is taken in its module)") + } + } + } + private fun registerPropertyOwners() { properties.forEach { (receiver, byName) -> val ref = classRefOf(receiver) @@ -704,6 +874,7 @@ private class StubRenderer(declarations: List) { fun render(): Map { renderModuleDefs() registerPropertyOwners() + registerMethodOwners() renderClasses() return modules.filterValues { it.defs.isNotEmpty() || it.classes.isNotEmpty() } .mapValues { (_, ctx) -> diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PythonNames.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PythonNames.kt new file mode 100644 index 000000000..ecb04e1b1 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PythonNames.kt @@ -0,0 +1,53 @@ +package python.multiplatform.gradle.stubs + +/** + * The binder's Pythonic naming rule (issue #131), mirrored for the stub generator. + * + * The runtime rule lives in `python_multiplatform` (`KotlinSurface.kt`: `snake_case`, `python_name`, + * `pythonic_aliases`), and it is pythonx-compose 0.1.0a1's (`_reexport.py`) character for character. + * This file restates it in Kotlin because the stubs are rendered at build time, with no interpreter; + * `PythonNameRuleTest` pins the two spellings against the same cases. + * + * - [snakeCase]: `fillMaxWidth` -> `fill_max_width`, `toURLString` -> `to_url_string`. + * - [pythonName]: upper-case first is unchanged (types, objects, composables); an explicit overload + * key converts its base and keeps its `__Types` suffix. + * - [pythonicAliases]: an alias is served only when it is not already a Kotlin name of the namespace + * and exactly one Kotlin name maps to it. A namespace (a Kotlin package path) is never converted. + */ + +private val ACRONYM_WORD = Regex("([A-Z]+)([A-Z][a-z])") +private val LOWER_UPPER = Regex("([a-z0-9])([A-Z])") + +/** Python's `_LOWER_UPPER.sub(r'\1_\2', _ACRONYM_WORD.sub(r'\1_\2', name)).lower()`. */ +internal fun snakeCase(kotlinName: String): String = + LOWER_UPPER.replace(ACRONYM_WORD.replace(kotlinName, "$1_$2"), "$1_$2").lowercase() + +/** The one Pythonic spelling of a Kotlin declaration or member name. */ +internal fun pythonName(kotlinName: String): String { + if (kotlinName.isEmpty() || kotlinName[0].isUpperCase()) return kotlinName + val separator = kotlinName.indexOf("__") + if (separator < 0) return snakeCase(kotlinName) + return snakeCase(kotlinName.substring(0, separator)) + kotlinName.substring(separator) +} + +/** Python's `str.isidentifier()` for the ASCII names a Kotlin declaration can carry here. */ +private fun isPythonIdentifier(name: String): Boolean = + name.isNotEmpty() && (name[0] == '_' || name[0].isLetter()) && name.all { it == '_' || it.isLetterOrDigit() } + +/** + * `alias -> kotlin name` for the names of one namespace, ambiguity refused: the runtime's + * `pythonic_aliases`. Independent of the order of [kotlinNames]. + */ +internal fun pythonicAliases(kotlinNames: Collection, rule: (String) -> String = ::pythonName): Map { + val names = kotlinNames.toSet() + val found = sortedMapOf() + val clashed = mutableSetOf() + for (kotlin in names) { + val alias = rule(kotlin) + if (alias == kotlin || alias in names || !isPythonIdentifier(alias)) continue + val other = found[alias] + if (other == null) found[alias] = kotlin else if (other != kotlin) clashed += alias + } + clashed.forEach { found.remove(it) } + return found +} diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PythonStubsTask.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PythonStubsTask.kt index fd57601ba..07790c626 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PythonStubsTask.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PythonStubsTask.kt @@ -1,5 +1,6 @@ package python.multiplatform.gradle.stubs +import python.multiplatform.gradle.deleteWithoutFollowingLinks import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty @@ -66,6 +67,10 @@ abstract class PythonStubsTask : DefaultTask() { @get:Input abstract val includePrefixes: ListProperty + /** Packages to leave out, the same set the binder gets; SPEC B-11. */ + @get:Input + abstract val excludePackages: ListProperty + /** * The klib reader for the isolated worker, exactly as * [python.multiplatform.gradle.artifact.PythonArtifactBindingsTask.klibReaderClasspath] -- including @@ -82,13 +87,14 @@ abstract class PythonStubsTask : DefaultTask() { val destination = outputDirectory.get().asFile // Rebuilt from scratch, exactly as `PythonArtifactBindingsTask` is: a dependency that was // removed, or an include prefix that stopped matching, must take its stub with it. - destination.deleteRecursively() + deleteWithoutFollowingLinks(destination) destination.mkdirs() val includes = includePrefixes.get() + val excludes = excludePackages.get() val jars = artifacts.files.filter { it.isFile && it.name.endsWith(".jar") } val declarations = jars.sortedBy { it.name }.flatMap { jar -> - ArtifactScanner.scanDeclarations(jar, includes, classpath = jars) + ArtifactScanner.scanDeclarations(jar, includes, classpath = jars, excludePackages = excludes) } + artifacts.files.filter { it.isFile && it.name.endsWith(".klib") }.sortedBy { it.name }.flatMap { klib -> // A Kotlin/Native target resolves klibs where a JVM target resolves jars, and both // producers now build the same `DeclarationModel` -- so a stub is not a JVM-only product. @@ -97,7 +103,7 @@ abstract class PythonStubsTask : DefaultTask() { // // Not a direct `KlibScanner.scanKlibDeclarations` call: same classloader-scope collision // `PythonArtifactBindingsTask`'s KDoc documents, worked around the same way. - workerExecutor.scanKlibIsolated(klib, includes, temporaryDir, klibReaderClasspath.files).declarations + workerExecutor.scanKlibIsolated(klib, includes, excludes, temporaryDir, klibReaderClasspath.files).declarations } val files = renderKotlinFqnStubs(declarations) diff --git a/tools/xcode/install-python.sh b/python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh similarity index 100% rename from tools/xcode/install-python.sh rename to python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/NoFollowDeleteTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/NoFollowDeleteTest.kt new file mode 100644 index 000000000..f16f2ad8b --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/NoFollowDeleteTest.kt @@ -0,0 +1,39 @@ +package python.multiplatform.gradle + +import java.io.File +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** Issue #227: the plugin's deletes remove a symbolic link as a link, never what it points at. */ +class NoFollowDeleteTest { + + private fun scratch(): File = + Files.createTempDirectory(File("../.tmp").apply { mkdirs() }.absoluteFile.toPath(), "nofollow").toFile() + + @Test + fun aLinkToAnOutsideDirectoryIsRemovedButItsTargetSurvives() { + val work = scratch() + try { + val outside = File(work, "outside").apply { mkdirs() } + val keep = File(outside, "keep.txt").apply { writeText("keep") } + val victim = File(work, "victim").apply { mkdirs() } + File(victim, "own.txt").writeText("own") + Files.createSymbolicLink(File(victim, "escape").toPath(), outside.toPath()) + Files.createSymbolicLink(File(victim, "dangling").toPath(), File(work, "nowhere").toPath()) + + deleteWithoutFollowingLinks(victim) + + assertFalse(victim.exists(), "the directory itself should be gone") + assertTrue(keep.isFile, "the link was followed: its target's content was deleted") + } finally { + deleteWithoutFollowingLinks(work) + } + } + + @Test + fun aMissingPathIsNotAnError() { + deleteWithoutFollowingLinks(File(scratch(), "absent")) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/PackagedPythonHomeTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/PackagedPythonHomeTest.kt index 833ea52d7..db7ecad44 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/PackagedPythonHomeTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/PackagedPythonHomeTest.kt @@ -101,6 +101,34 @@ class PackagedPythonHomeTest { assertTrue(sync.source.files.none { it.startsWith(fixture.prefix) }, "disabled packaging must copy nothing") } + // ---- beside a native image (issue #246) ---- + + @Test + fun aNativeImageDirectoryGetsThePrunedPrefixBesideTheExecutable() { + val fixture = Fixture() + val images = File(fixture.root, "native-image-out") + fixture.extension.nativeImageDirectory.set(images) + val sync = fixture.project.tasks.getByName("stageNativeImagePythonHome") as Sync + + assertEquals(File(images, PACKAGED_HOME_DIRECTORY), sync.destinationDir) + val dependencies = sync.taskDependencies.getDependencies(sync).map { it.name } + assertTrue("stagePythonHome" in dependencies, "the prefix must be staged first; got $dependencies") + val copied = sync.source.files.map { it.relativeTo(fixture.prefix).invariantSeparatorsPath }.toSet() + assertTrue(fixture.stdlibFile in copied, "the stdlib must be copied; got $copied") + assertTrue(fixture.libraryFile in copied, "the shared library must be copied; got $copied") + assertFalse(fixture.headerFile in copied, "headers are build-time only; got $copied") + assertFalse(fixture.bytecodeFile in copied, "__pycache__ is not copied; got $copied") + } + + @Test + fun withoutANativeImageDirectoryTheTaskCopiesNothing() { + val fixture = Fixture() + val sync = fixture.project.tasks.getByName("stageNativeImagePythonHome") as Sync + assertTrue(sync.source.files.none { it.startsWith(fixture.prefix) }, "an unset directory must copy nothing") + val dependencies = sync.taskDependencies.getDependencies(sync).map { it.name } + assertFalse("stagePythonHome" in dependencies, "an unset directory must not stage; got $dependencies") + } + /** * A project whose Gradle user home already holds a "staged" prefix, at exactly the path * `configurePythonHomeStaging` computes, so the wiring can be read without a download. diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WasmProxyExportsRenderingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WasmProxyExportsRenderingTest.kt index 30ec8f1a0..899c8cc99 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WasmProxyExportsRenderingTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WasmProxyExportsRenderingTest.kt @@ -78,7 +78,7 @@ class WasmProxyExportsRenderingTest { @Test fun traverseHasCorrectArity() { - // tp_traverse: (PyObject *self, visitproc visit, void *arg) -> int — three i32 params + // tp_traverse: (PyObject *self, visitproc visit, void *arg) -> int, three i32 params val source = renderWasmProxyExportsSource("test.pkg") assertTrue( source.contains("selfPtr: Int, visitPtr: Int, argPtr: Int"), @@ -88,7 +88,7 @@ class WasmProxyExportsRenderingTest { @Test fun clearHasCorrectArity() { - // tp_clear: (PyObject *self) -> int — one i32 param + // tp_clear: (PyObject *self) -> int, one i32 param val source = renderWasmProxyExportsSource("test.pkg") val clearFun = source.substringAfter("pmp_tp_clear\"") assertTrue( @@ -99,12 +99,12 @@ class WasmProxyExportsRenderingTest { @Test fun deallocHasVoidReturn() { - // tp_dealloc: (PyObject *self) -> void — no return type annotation (Unit) + // tp_dealloc: (PyObject *self) -> void, no return type annotation (Unit) val source = renderWasmProxyExportsSource("test.pkg") val deallocFun = source.substringAfter("pmp_tp_dealloc\"") assertTrue( deallocFun.contains("selfPtr: Int) ="), - "dealloc must be '(selfPtr: Int) =' — no return type (Unit/void)", + "dealloc must be '(selfPtr: Int) =', no return type (Unit/void)", ) } diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WiringTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WiringTest.kt index 2fb225019..30710976d 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WiringTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/WiringTest.kt @@ -8,12 +8,12 @@ import kotlin.test.assertTrue /** * The three decisions the plugin makes for the user, as pure functions. * - * The plugin's real proof is `ksp-fixtures`, which applies it and runs its generated table; these + * The plugin's real proof is the `fixtures/` modules, which apply it and runs its generated table; these * pin the parts that would otherwise only be observable as a missing fragment or a duplicate one. */ class WiringTest { - // Observed on `:ksp-fixtures:app` (`gradlew dependencies`), Kotlin 2.4.20-Beta2 + KSP 2.3.11. + // Observed on `:python-multiplatform-ksp:fixtures:app` (`gradlew dependencies`), Kotlin 2.4.20-Beta2 + KSP 2.3.11. private val observedConfigurations = listOf( "ksp", "kspAndroidNativeArm64", @@ -46,7 +46,7 @@ class WiringTest { assertTrue(isBindingKspConfiguration("ksp", isMultiplatform = false)) } - // Observed on `:ksp-fixtures:android` (`gradlew :ksp-fixtures:android:dependencies`), the + // Observed on `:python-multiplatform-ksp:fixtures:android` (`gradlew :python-multiplatform-ksp:fixtures:android:dependencies`), the // fixture that carries `com.android.library`. AGP names its source sets with the build type // *last*, so the test configurations here do not end in `Test` the way the Kotlin-target ones // above do. Every one of the nine `...Test...` names below was receiving the processor before @@ -95,7 +95,7 @@ class WiringTest { // The ROADMAP §13 defect: `kspAndroidTestDebug` does not end in `Test`, so the processor // ran over the unit-test sources and emitted a `Fragment_`/`FunctionTable` pair // that shadowed `main`'s inside the test compilation. Observed on - // `:ksp-fixtures:android` before this filter was widened. + // `:python-multiplatform-ksp:fixtures:android` before this filter was widened. for (name in listOf( "kspAndroidTestDebug", "kspAndroidTestRelease", @@ -147,8 +147,8 @@ class WiringTest { // Two independent libraries both called `:core` would otherwise both emit // `python.multiplatform.generated.fragments.Fragment_core` and collide on one classpath. assertEquals( - "io_github_thisisthepy_ksp_fixtures_app", - deriveModuleName(group = "io.github.thisisthepy", path = ":ksp-fixtures:app", name = "app"), + "io_github_thisisthepy_python_multiplatform_ksp_fixtures_app", + deriveModuleName(group = "io.github.thisisthepy", path = ":python-multiplatform-ksp:fixtures:app", name = "app"), ) assertEquals( "com_example_core", diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactScannerTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactScannerTest.kt index b89dffd3f..1c303ec99 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactScannerTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactScannerTest.kt @@ -548,7 +548,7 @@ class ArtifactScannerTest { * fail this. What the floors pin is that the two gates are open, which is the fact under test. * * `docs/design/ecosystem.md` §5b's own target (calling a composable) is out of scope here on purpose -- - * this counts declarations, and `:ksp-fixtures:artifact` is where a call is actually made. + * this counts declarations, and `:python-multiplatform-gradle-plugin-fixtures:artifact` is where a call is actually made. */ @Test fun composeModifierExtensionsSurviveBothGates() { diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactTableRenderingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactTableRenderingTest.kt index 3c3ac51ad..efbb90b40 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactTableRenderingTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ArtifactTableRenderingTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertEquals * * 1. **`FunctionTable` already means something.** `PackageScanFragmentDiscovery` builds it from * `Resolver.getDeclarationsFromPackage(python.multiplatform.generated.fragments)` -- "every - * module in this graph that was compiled with the processor". `ksp-fixtures/app`'s + * module in this graph that was compiled with the processor". `python-multiplatform-ksp/fixtures/app`'s * `CommonInstallTest` asserts that set exactly. Putting walked artefacts in it would silently * change a shipped contract, and would make a consumer's table grow when they added an unrelated * dependency. diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/FunctionSlotBindingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/FunctionSlotBindingTest.kt index d6f04774d..d9c03a48d 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/FunctionSlotBindingTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/FunctionSlotBindingTest.kt @@ -249,7 +249,7 @@ class FunctionSlotBindingTest { } /** - * `kotlin.system.measureTimeMillis`, which is the shape `:ksp-fixtures:artifact` calls from real + * `kotlin.system.measureTimeMillis`, which is the shape `:python-multiplatform-gradle-plugin-fixtures:artifact` calls from real * Python: the same `() -> Unit` slot `clickable.onClick` has, on a declaration that **invokes** * it rather than storing it for a composition to invoke later. * diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/InternalPackageExclusionTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/InternalPackageExclusionTest.kt new file mode 100644 index 000000000..f96b86794 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/InternalPackageExclusionTest.kt @@ -0,0 +1,104 @@ +package python.multiplatform.gradle.artifact + +import python.multiplatform.gradle.stubs.renderKotlinFqnStubs +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Issue #192 over `internalpackagefixture/`: the walker keeps public declarations of an `internal` + * package out of the bindings and the stubs, and honours `excludePackages` like the KSP side (SPEC B-11). + * + * ### Red before the implementation + * + * Every test here that passes `excludePackages =` fails to compile before the change (the parameter does + * not exist), which is the expected red. Once it compiles, [aPublicDeclarationInAnInternalPackageIsNotBound] + * and [theStubsOfAnInternalPackageAreNotEmitted] fail with the `.internal` names present, and the guards + * ([aNormalPackageStaysBound], [aPackageThatOnlyLooksInternalStaysBound], [withoutTheOptionAnExcludedPackageIsBound]) + * stay green. + */ +class InternalPackageExclusionTest { + + private val pkg = "fixture.artifactinternal" + + private val fixtureClasses: File + get() { + val location = Class.forName("$pkg.Kept").protectionDomain.codeSource.location + return File(location.toURI()).also { assertTrue(it.isDirectory, "expected a directory of .class files: $it") } + } + + private fun names(exclude: List = emptyList()): Set = + ArtifactScanner.scanJar(fixtureClasses, includePrefixes = listOf(pkg), excludePackages = exclude) + .map { it.name }.toSet() + + @Test + fun aNormalPackageStaysBound() { + val bound = names() + assertTrue("$pkg.Kept" in bound, "bound: $bound") + assertTrue("$pkg.keptFunction" in bound, "bound: $bound") + } + + @Test + fun aPublicDeclarationInAnInternalPackageIsNotBound() { + val bound = names() + assertEquals(emptyList(), bound.filter { ".internal." in it || it.endsWith(".internal") }.sorted(), "bound: $bound") + // Below an `internal` segment as well: `*.internal.*`. + assertEquals(emptyList(), bound.filter { "HiddenBelowInternal" in it || "hiddenBelowInternal" in it }, "bound: $bound") + } + + @Test + fun aPackageThatOnlyLooksInternalStaysBound() { + val bound = names() + assertTrue("$pkg.internals.LooksInternal" in bound, "bound: $bound") + assertTrue("$pkg.internals.looksInternalFunction" in bound, "bound: $bound") + } + + @Test + fun withoutTheOptionAnExcludedPackageIsBound() { + val bound = names() + assertTrue("$pkg.excluded.ExcludedByOption" in bound, "bound: $bound") + assertTrue("$pkg.excluded.excludedByOptionFunction" in bound, "bound: $bound") + } + + @Test + fun excludePackagesKeepsAPackageAndItsSubpackagesOut() { + val bound = names(exclude = listOf("$pkg.excluded")) + assertEquals(emptyList(), bound.filter { ".excluded." in it }, "bound: $bound") + assertTrue("$pkg.Kept" in bound, "the control must still bind; bound: $bound") + assertTrue("$pkg.internals.LooksInternal" in bound, "bound: $bound") + } + + @Test + fun excludePackagesMatchesANamespaceNotAPrefixOfText() { + // `fixture.artifactinternal.intern` is a text prefix of `...internals` and no package. + val bound = names(exclude = listOf("$pkg.intern")) + assertTrue("$pkg.internals.LooksInternal" in bound, "bound: $bound") + // An entry that names the package itself excludes it. + assertEquals(emptyList(), names(exclude = listOf("$pkg.internals")).filter { ".internals." in it }) + } + + @Test + fun noDeclarationModelIsKeptForAnInternalOrExcludedPackage() { + val owners = ArtifactScanner.scanDeclarations( + fixtureClasses, includePrefixes = listOf(pkg), excludePackages = listOf("$pkg.excluded"), + ).map { it.owner }.toSet() + assertEquals(emptyList(), owners.filter { owner -> owner.split('.').any { it == "internal" || it == "excluded" } }, "owners: $owners") + assertTrue(owners.any { it == pkg || it.startsWith("$pkg.Kept") }, "the control must still be modelled; owners: $owners") + } + + @Test + fun theStubsOfAnInternalPackageAreNotEmitted() { + val files = renderKotlinFqnStubs( + ArtifactScanner.scanDeclarations( + fixtureClasses, includePrefixes = listOf(pkg), excludePackages = listOf("$pkg.excluded"), + ), + ) + val paths = files.keys + assertTrue(paths.any { it.startsWith("fixture/artifactinternal/") }, "the control must be stubbed; paths: $paths") + assertEquals(emptyList(), paths.filter { "/internal/" in it || "/excluded/" in it }, "paths: $paths") + val text = files.values.joinToString("\n") + assertTrue("HiddenByInternal" !in text && "HiddenBelowInternal" !in text && "ExcludedByOption" !in text, "stub text leaks a skipped declaration") + assertTrue(paths.any { "/internals/" in it }, "paths: $paths") + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibMemberFunctionTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibMemberFunctionTest.kt new file mode 100644 index 000000000..1e8e51c06 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibMemberFunctionTest.kt @@ -0,0 +1,93 @@ +package python.multiplatform.gradle.artifact + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Issue #237: the klib walker binds member functions of its classes and interfaces, as #215 binds them + * from a jar -- `METHOD` entries `Owner.name`, the receiver in `args[0]` and uncounted in `arity`, the + * call `(args[0] as Owner).name(...)`. + * + * Before it, only top-level functions were read, so on Kotlin/Native `CompletableJob.complete()` was + * unreachable from the handle `Job()` returns (#234). + * + * ### Red before the implementation + * + * No entry or declaration exists for any member (`single` finds nothing). + */ +class KlibMemberFunctionTest { + + private val coroutinesKlib: File + get() { + val path = System.getProperty("python.multiplatform.walkerFixtureKlib") + assertNotNull(path, "the walkerFixtureKlib system property is not set; see build.gradle.kts") + return File(path).also { assertTrue(it.isFile, "not a file: $it") } + } + + private val entries by lazy { KlibScanner.scanKlib(coroutinesKlib, includePrefixes = listOf("kotlinx.coroutines")) } + private val declarations by lazy { KlibScanner.scanKlibDeclarations(coroutinesKlib, includePrefixes = listOf("kotlinx.coroutines")) } + + private val completableJob = "kotlinx.coroutines.CompletableJob" + private val job = "kotlinx.coroutines.Job" + private val semaphore = "kotlinx.coroutines.sync.Semaphore" + + @Test + fun aMemberIsAMethodEntryWithItsReceiverOutsideItsSlots() { + val complete = entries.single { it.name == "$completableJob.complete" } + assertEquals("METHOD", complete.kind) + assertEquals(0, complete.arity) + assertEquals(completableJob, complete.receiverTypeName) + assertEquals("BOOLEAN", complete.returnTag) + assertTrue(complete.lambdaBody.startsWith("{ args ->"), complete.lambdaBody) + assertTrue("(args[0] as $completableJob).complete()" in complete.lambdaBody, complete.lambdaBody) + } + + @Test + fun anInterfacesMembersBind() { + assertNotNull(entries.singleOrNull { it.name == "$job.start" }) + assertNotNull(entries.singleOrNull { it.name == "$semaphore.tryAcquire" }) + assertNotNull(entries.singleOrNull { it.name == "$semaphore.release" }) + } + + @Test + fun theModelIsAMethodOfItsOwner() { + val start = declarations.single { it.bindingName == "$job.start" } + assertEquals("METHOD", start.kind) + assertEquals(job, start.owner) + assertTrue(start.ownerIsClass) + assertEquals(job, start.receiver?.qualifiedName) + } + + @Test + fun aSuspendMemberIsDeclinedWithItsReason() { + val join = declarations.single { it.owner == job && it.simpleName == "join" } + assertNull(join.bindingName) + assertEquals("suspend", join.declineReason) + } + + @Test + fun aMemberTypedByAnotherKlibIsDeclinedWithItsReason() { + // `Job.cancel(cause: CancellationException? = null)`: the cause is a stdlib class. + val cancel = declarations.filter { it.owner == job && it.simpleName == "cancel" } + assertTrue(cancel.isNotEmpty()) + assertTrue(cancel.all { it.bindingName == null }, cancel.map { it.bindingName }.toString()) + assertTrue(cancel.any { it.declineReason?.startsWith("no boundary type for parameter") == true }, cancel.map { it.declineReason }.toString()) + } + + @Test + fun aMemberOfAGenericClassIsDeclinedWithItsReason() { + val await = declarations.filter { it.owner == "kotlinx.coroutines.Deferred" } + assertTrue(await.isNotEmpty()) + assertTrue(await.all { it.bindingName == null }) + assertTrue(await.any { it.declineReason?.contains("generic") == true }, await.map { it.declineReason }.toString()) + } + + @Test + fun aMemberIsNotAModuleFunction() { + assertTrue(entries.none { it.name == "kotlinx.coroutines.complete" || it.name == "kotlinx.coroutines.start" }) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibObjectHandleTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibObjectHandleTest.kt new file mode 100644 index 000000000..fdc7c5e5e --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibObjectHandleTest.kt @@ -0,0 +1,78 @@ +package python.multiplatform.gradle.artifact + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Issue #234: the klib walker carries Kotlin objects as handles (SPEC B-2). + * + * Before it, [KlibScanner] knew only `kotlin`'s primitives, `String`, `ByteArray` and `Unit`, so on + * Kotlin/Native nothing that takes or returns a Kotlin object bound -- `kotlinx.coroutines.Job(parent: + * Job? = null): CompletableJob` was declined, and member functions (#215) had no receiver to run on. + * A type whose class is a public, non-generic, non-value `class`/`interface`/`enum class`/`object` + * **of the walked klib** now crosses as an `OBJECT` handle, cast in generated source the way the jar + * producer casts one; a type from another klib is still declined with its reason. + * + * ### Red before the implementation + * + * `Job` and `MainScope` have no binding (`single` finds no entry), and `CoroutineScope(context)` is + * declined for its *result* (`no boundary type for return kotlinx.coroutines.CoroutineScope`); after, + * the result crosses and the stdlib parameter is what still declines it. + */ +class KlibObjectHandleTest { + + private val coroutinesKlib: File + get() { + val path = System.getProperty("python.multiplatform.walkerFixtureKlib") + assertNotNull(path, "the walkerFixtureKlib system property is not set; see build.gradle.kts") + return File(path).also { assertTrue(it.isFile, "not a file: $it") } + } + + private val entries by lazy { KlibScanner.scanKlib(coroutinesKlib, includePrefixes = listOf("kotlinx.coroutines")) } + private val declarations by lazy { KlibScanner.scanKlibDeclarations(coroutinesKlib, includePrefixes = listOf("kotlinx.coroutines")) } + + private val job = "kotlinx.coroutines.Job" + + @Test + fun aNullableHandleParameterAndAHandleResultBind() { + val factory = entries.single { it.name == job } + assertEquals(listOf("OBJECT"), factory.paramTags) + assertEquals(listOf(job), factory.paramTypeNames) + assertEquals("OBJECT", factory.returnTag) + assertEquals("kotlinx.coroutines.CompletableJob", factory.returnTypeName) + assertTrue("(args[0] as $job?)" in factory.lambdaBody, factory.lambdaBody) + } + + @Test + fun aHandleResultCarriesItsAncestryFromTheAbi() { + // `_member` and `_is_a` read the ancestry the table carries: a `CompletableJob` is a `Job`. + val factory = entries.single { it.name == job } + assertTrue(job in factory.returnSupertypes, factory.returnSupertypes.toString()) + } + + @Test + fun anInterfaceResultBinds() { + val mainScope = entries.single { it.name == "kotlinx.coroutines.MainScope" } + assertEquals("kotlinx.coroutines.CoroutineScope", mainScope.returnTypeName) + assertEquals("OBJECT", mainScope.returnTag) + } + + @Test + fun aTypeFromAnotherKlibIsStillDeclinedWithItsReason() { + val scope = declarations.single { it.simpleName == "CoroutineScope" && it.parameters.size == 1 } + assertNull(scope.bindingName) + assertEquals("no boundary type for parameter kotlin.coroutines.CoroutineContext", scope.declineReason) + } + + @Test + fun noBoundDeclarationCarriesAGenericType() { + val bound = declarations.filter { it.bindingName != null } + assertTrue(bound.isNotEmpty()) + val generic = bound.filter { d -> (d.parameters.map { it.type } + d.returnType).any { it.arguments.isNotEmpty() } } + assertEquals(emptyList(), generic.map { it.bindingName }) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibScannerTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibScannerTest.kt index 5a5e77f7d..4f352aef6 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibScannerTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/KlibScannerTest.kt @@ -130,7 +130,7 @@ class KlibScannerTest { * test pinned one -- `checkIndexOverflow`, `kotlinx.coroutines.flow.internal`'s bounds check -- as * bound. It was wrong: `checkIndexOverflow` is `@PublishedApi internal`, binary-visible so an * inline call site elsewhere in the *same* library can resolve it, but not source-visible outside - * that module. `KlibScanWorkAction`'s KDoc records how this was found -- `:ksp-fixtures + * that module. `KlibScanWorkAction`'s KDoc records how this was found -- `:python-multiplatform-gradle-plugin-fixtures * :klib-artifact:compileKotlinAndroidNativeArm64` failing to compile the generated fragment with * "it is internal in file", the first time this walker's output was ever actually compiled rather * than only unit-tested. `KlibScanner`'s `PUBLISHED_API` constant has the rest. Every other candidate in @@ -141,22 +141,46 @@ class KlibScannerTest { * "does it survive a real, messy artefact, and decline what it must decline" case. */ @Test - fun aRealThirdPartyKlibIsScannedWithoutCrashingAndDeclinesEveryDeclaration() { + fun aRealThirdPartyKlibIsScannedWithoutCrashingAndBindsItsHandleFactoriesAndMembers() { + // Zero before issue #234, when no Kotlin object could cross the klib boundary. Since then the + // non-extension factories whose types are this klib's own classes bind, and since #237 the + // members of its interfaces and final classes; the extensions, the suspend functions, the + // members of open classes and anything typed by another klib (`CoroutineContext`) are declined. val entries = KlibScanner.scanKlib(coroutinesKlib, includePrefixes = emptyList()) - assertEquals(emptyList(), entries.map { it.name }) + assertEquals( + listOf( + "kotlinx.coroutines.ChildJob.parentCancelled", + "kotlinx.coroutines.CompletableJob.complete", + "kotlinx.coroutines.DisposableHandle.dispose", + "kotlinx.coroutines.Job", + "kotlinx.coroutines.Job.attachChild", + "kotlinx.coroutines.Job.start", + "kotlinx.coroutines.MainScope", + "kotlinx.coroutines.Runnable.run", + "kotlinx.coroutines.SupervisorJob", + "kotlinx.coroutines.TimeoutCancellationException.createCopy", + "kotlinx.coroutines.newFixedThreadPoolContext", + "kotlinx.coroutines.newSingleThreadContext", + "kotlinx.coroutines.sync.Mutex", + "kotlinx.coroutines.sync.Semaphore", + "kotlinx.coroutines.sync.Semaphore.release", + "kotlinx.coroutines.sync.Semaphore.tryAcquire", + ), + entries.map { it.name }, + ) } - /** The specific bug [aRealThirdPartyKlibIsScannedWithoutCrashingAndDeclinesEveryDeclaration] pins + /** The specific bug [aRealThirdPartyKlibIsScannedWithoutCrashingAndBindsItsHandleFactoriesAndMembers] pins * the fix for, isolated to one declaration and its reason. */ @Test fun aPublishedApiInternalDeclarationIsDeclinedWithItsOwnReason() { val declarations = KlibScanner.scanKlibDeclarations(coroutinesKlib, includePrefixes = emptyList()) - val checkIndexOverflow = declarations.single { it.simpleName == "checkIndexOverflow" } - assertEquals(null, checkIndexOverflow.bindingName) - assertTrue( - checkIndexOverflow.declineReason?.contains("PublishedApi") == true, - "expected a @PublishedApi decline reason, got ${checkIndexOverflow.declineReason}", - ) + // checkIndexOverflow lives in kotlinx.coroutines.internal, which B-11 (#192) skips outright: + // it gets no model at all, so the @PublishedApi reason is checked on declarations elsewhere. + assertTrue(declarations.none { it.simpleName == "checkIndexOverflow" }) + val publishedApi = declarations.filter { it.declineReason?.contains("PublishedApi") == true } + assertTrue(publishedApi.isNotEmpty(), "expected a @PublishedApi decline outside internal packages") + assertTrue(publishedApi.all { it.bindingName == null }) } /** diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/MemberFunctionBindingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/MemberFunctionBindingTest.kt new file mode 100644 index 000000000..01c7648f3 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/MemberFunctionBindingTest.kt @@ -0,0 +1,135 @@ +package python.multiplatform.gradle.artifact + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Issue #215: the public member functions of a class or interface are bound, as `METHOD` entries. + * + * Before it, the walker bound statics, an `object`'s functions (#53), constructors and properties + * (#38), and nothing else on a class: `kotlinx.coroutines.Job.cancel()` and + * `CoroutineDispatcher.dispatch(...)` were unreachable from a handle Python already held. A member + * is the shape KSP's `METHOD` already has -- `args[0]` is the receiver, not counted in `arity` -- and + * generated Kotlin source calls it as `(args[0] as Owner).name(...)`; nothing here looks a JVM method + * up by name. + * + * ### Red before the implementation, and what each red says + * + * Against the walker before #215 every "is bound" test fails with no declaration of that name + * (`single` throws, or the binding name is `null`); the guards that pin what stays (the constructor, + * the property, the object's static) pass both before and after, and a failure there is a regression. + */ +class MemberFunctionBindingTest { + + private val fixtureClasses: File + get() = File(Class.forName("fixture.artifactmember.Counter").protectionDomain.codeSource.location.toURI()) + + private fun declarations() = + ArtifactScanner.scanDeclarations(fixtureClasses, includePrefixes = listOf("fixture.artifactmember")) + + private fun callables() = + ArtifactScanner.scanJar(fixtureClasses, includePrefixes = listOf("fixture.artifactmember")) + + private val counter = "fixture.artifactmember.Counter" + private val handle = "fixture.artifactmember.Handle" + + @Test + fun aClassMemberIsBoundAsAMethodOfItsOwner() { + val reset = declarations().single { it.simpleName == "reset" } + assertEquals("$counter.reset", reset.bindingName) + assertEquals("METHOD", reset.kind) + assertEquals(counter, reset.owner) + assertTrue(reset.ownerIsClass) + assertEquals(counter, reset.receiver?.qualifiedName) + assertTrue(reset.parameters.isEmpty()) + } + + @Test + fun theTableEntryCarriesTheReceiverOutsideItsSlots() { + val stop = callables().single { it.name == "$handle.stop" } + assertEquals("METHOD", stop.kind) + assertEquals(0, stop.arity) + assertEquals(handle, stop.receiverTypeName) + assertTrue("(args[0] as $handle).stop()" in stop.lambdaBody, stop.lambdaBody) + // A zero-parameter member still reads its receiver: the body must take `args`. + assertTrue(stop.lambdaBody.startsWith("{ args ->"), stop.lambdaBody) + val follows = callables().single { it.name == "$handle.follows" } + assertEquals(1, follows.arity) + assertEquals(listOf("other"), follows.paramNames) + assertEquals(listOf(handle), follows.paramTypeNames) + assertTrue("(args[0] as $handle).follows(" in follows.lambdaBody, follows.lambdaBody) + assertTrue("args[1]" in follows.lambdaBody, follows.lambdaBody) + } + + @Test + fun anInterfacesMembersAreBound() { + val bound = declarations().filter { it.owner == handle && it.kind == "METHOD" }.mapNotNull { it.bindingName }.sorted() + assertEquals(listOf("$handle.follows", "$handle.stop"), bound) + } + + @Test + fun theOverloadsOfAMemberAreToldApart() { + val bound = declarations().filter { it.simpleName == "add" }.mapNotNull { it.bindingName }.sorted() + assertEquals(listOf("$counter.add__Int", "$counter.add__Int_String"), bound) + } + + @Test + fun aMembersDefaultIsNotOmittableYet() { + val scaled = callables().single { it.name == "$counter.scaled" } + assertEquals(listOf(false), scaled.paramHasDefault) + assertFalse(declarations().single { it.bindingName == "$counter.scaled" }.parameters.single().declaresDefault) + } + + @Test + fun aSuspendMemberIsDeclinedWithItsReason() { + val later = declarations().single { it.simpleName == "later" } + assertNull(later.bindingName) + assertEquals("suspend", later.declineReason) + } + + @Test + fun aMemberOfAGenericClassIsDeclinedWithItsReason() { + val describe = declarations().single { it.simpleName == "describe" } + assertNull(describe.bindingName) + assertNotNull(describe.declineReason) + assertTrue("generic" in describe.declineReason!!, describe.declineReason) + } + + @Test + fun aMemberNamedLikeAPropertyIsDeclinedAndThePropertyKept() { + val sized = declarations().filter { it.owner == "fixture.artifactmember.Sized" && it.simpleName == "size" } + assertEquals("GETTER", sized.single { it.bindingName != null }.kind) + val method = sized.single { it.kind == "METHOD" } + assertNull(method.bindingName) + assertTrue("property" in method.declineReason!!, method.declineReason) + } + + @Test + fun anyMembersAreNotBound() { + assertFalse(declarations().any { it.simpleName in setOf("toString", "equals", "hashCode") && it.bindingName != null }) + } + + @Test + fun theClassesConstructorIsStillBound() { + val constructor = declarations().single { it.isConstructor && it.simpleName == "Counter" } + assertEquals(counter, constructor.bindingName) + } + + @Test + fun thePropertiesAreStillBound() { + assertEquals("$counter.count", declarations().single { it.simpleName == "count" && it.kind == "GETTER" }.bindingName) + assertEquals("$handle.active", declarations().single { it.owner == handle && it.simpleName == "active" }.bindingName) + } + + @Test + fun anObjectsFunctionStaysAStaticUnderTheObjectsName() { + val lookup = declarations().single { it.simpleName == "lookup" } + assertEquals("FUNCTION", lookup.kind) + assertEquals("fixture.artifactmember.Registry.lookup", lookup.bindingName) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ObjectMemberBindingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ObjectMemberBindingTest.kt index be587db14..051c0145d 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ObjectMemberBindingTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ObjectMemberBindingTest.kt @@ -61,8 +61,11 @@ class ObjectMemberBindingTest { } @Test - fun anOrdinaryClassInstanceMethodIsStillNotBound() { - assertTrue(declarations().none { it.simpleName == "instanceMethod" && it.bindingName != null }) + fun anOrdinaryClassInstanceMethodIsAMethodOfItsInstancesNotAStatic() { + // Issue #215: bound since, as a `METHOD` read on an instance -- never as an object's static. + val instanceMethod = declarations().single { it.simpleName == "instanceMethod" } + assertEquals("METHOD", instanceMethod.kind) + assertEquals("fixture.artifactobject.Plain.instanceMethod", instanceMethod.bindingName) } @Test diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/PropertyBindingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/PropertyBindingTest.kt index c7aadb5f3..f26a7c19a 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/PropertyBindingTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/PropertyBindingTest.kt @@ -172,7 +172,7 @@ class PropertyBindingTest { // ----------------------------------------------------- a receiver the classpath cannot see /** - * The `:ksp-fixtures:compose` compile failure on ui-desktop 1.11's + * The `:python-multiplatform-compose:fixtures:compose` compile failure on ui-desktop 1.11's * `DefaultArchitectureComponentsOwner`: its supertypes live in lifecycle jars that the consumer's * compile classpath does not carry, so `(args[0] as Owner).lifecycle` does not compile. Declined * with a reason naming the missing supertype -- for a member property and an extension property diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ReturnSupertypeTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ReturnSupertypeTest.kt index f6c9d3979..a045c2426 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ReturnSupertypeTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/ReturnSupertypeTest.kt @@ -132,7 +132,7 @@ class ReturnSupertypeTest { } /** - * **The price.** Over the four packages `:ksp-fixtures:compose` actually walks, how many entries + * **The price.** Over the four packages `:python-multiplatform-compose:fixtures:compose` actually walks, how many entries * gain anything, how many characters that is against the rendered fragments, and how long the * longest chain gets. * diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/SamParameterBindingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/SamParameterBindingTest.kt new file mode 100644 index 000000000..e804fa76c --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/SamParameterBindingTest.kt @@ -0,0 +1,96 @@ +package python.multiplatform.gradle.artifact + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Issue #216: a parameter typed as a functional interface takes a Python callable, as a function + * type does (B-5). + * + * `CoroutineDispatcher.dispatch(context, block: Runnable)` is the motivating case: `runRunnable(block: + * Runnable)` was declined ("no boundary type for parameter java.lang.Runnable", because the JDK is not + * on the walk's classpath) while `runLambda(block: () -> Unit)` bound. The slot is described to Python + * as the `kotlin.FunctionN` it is equivalent to, so the existing wrapper path fills it, and generated + * source converts that wrapper with a SAM lambda -- nothing is looked up at run time. + * + * The slot takes a Kotlin handle too: its type name is the interface's, then `|`, then the function + * type, and the generated read casts anything that is not a wrapper to the interface. A functional + * interface whose method returns a value, or a generic one, is left exactly as it was -- Compose's + * `Alignment` is a `fun interface` returning `IntOffset`, and its slots must keep taking handles. + * + * ### Red before the implementation + * + * `runRunnable`, `maybeRun` and `notifyListener` have no functional-interface slot (the first two are + * declined, so `single` finds no callable; the third crosses as a plain handle). The guards -- `keep`, + * `produce` and `drain` still crossing as handles -- pass before and after; a failure there is a + * regression. + */ +class SamParameterBindingTest { + + private val fixtureClasses: File + get() = File(Class.forName("fixture.artifactsam.Listener").protectionDomain.codeSource.location.toURI()) + + private fun declarations() = + ArtifactScanner.scanDeclarations(fixtureClasses, includePrefixes = listOf("fixture.artifactsam")) + + private fun callables() = + ArtifactScanner.scanJar(fixtureClasses, includePrefixes = listOf("fixture.artifactsam")) + + private val pkg = "fixture.artifactsam" + + @Test + fun aJdkRunnableParameterIsAZeroArgumentFunctionSlot() { + val run = callables().single { it.name == "$pkg.runRunnable" } + assertEquals(listOf("java.lang.Runnable|kotlin.Function0()->kotlin.Unit"), run.paramTypeNames) + assertEquals(listOf("OBJECT"), run.paramTags) + assertTrue("java.lang.Runnable {" in run.lambdaBody, run.lambdaBody) + // A Kotlin handle is cast to the interface rather than refused. + assertTrue("else v as java.lang.Runnable" in run.lambdaBody, run.lambdaBody) + } + + @Test + fun aKotlinFunInterfaceParameterCarriesItsMethodsParameters() { + val notify = callables().single { it.name == "$pkg.notifyListener" } + assertEquals("$pkg.Listener|kotlin.Function2(kotlin.String,kotlin.Int)->kotlin.Unit", notify.paramTypeNames.first()) + assertTrue("$pkg.Listener {" in notify.lambdaBody, notify.lambdaBody) + } + + @Test + fun aNullableFunctionalInterfaceParameterStaysNullable() { + val declined = declarations().single { it.simpleName == "maybeRun" } + assertNull(declined.declineReason, declined.declineReason) + val maybe = callables().single { it.name == "$pkg.maybeRun" } + assertTrue("?.let" in maybe.lambdaBody, maybe.lambdaBody) + } + + @Test + fun theStubSeesACallable() { + val parameter = declarations().single { it.bindingName == "$pkg.runRunnable" }.parameters.single() + assertEquals("kotlin.Function0", parameter.type.qualifiedName) + assertEquals(listOf("kotlin.Unit"), parameter.type.arguments.map { it?.qualifiedName }) + } + + @Test + fun aValueReturningFunctionalInterfaceStillCrossesAsAHandle() { + val produce = callables().single { it.name == "$pkg.produce" } + assertEquals(listOf("$pkg.Producer"), produce.paramTypeNames) + } + + @Test + fun aGenericFunctionalInterfaceIsLeftToTheRulesBefore() { + val drain = declarations().single { it.simpleName == "drain" } + assertEquals(null, drain.bindingName) + // The reason it had before #216: a parameterised interface does not cross as a handle either. + assertEquals("no boundary type for parameter fixture.artifactsam.Sink", drain.declineReason) + } + + @Test + fun anOrdinaryInterfaceStillCrossesAsAHandle() { + val keep = callables().single { it.name == "$pkg.keep" } + assertEquals(listOf("$pkg.NotFunctional"), keep.paramTypeNames) + assertTrue("as $pkg.NotFunctional" in keep.lambdaBody, keep.lambdaBody) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/TextStateBindingTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/TextStateBindingTest.kt index 70a4f49d0..15bd4a625 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/TextStateBindingTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/TextStateBindingTest.kt @@ -25,7 +25,7 @@ import kotlin.test.assertTrue * file, so the object-handle case could not name it and `TextFieldState.text` was declined * ("no boundary type for property type kotlin.CharSequence"). * - * (The third cause, that `:ksp-fixtures:compose` did not walk `androidx.compose.foundation.text.input` + * (The third cause, that `:python-multiplatform-compose:fixtures:compose` did not walk `androidx.compose.foundation.text.input` * at all, is configuration, and `TextFieldStateRenderTest` there is its test.) * * ### Red before the implementation @@ -153,7 +153,7 @@ class TextStateBindingTest { entry("$pkg.Editable") val stub = renderKotlinFqnStubs(declarations).getValue("fixture/artifacttextstate/__init__.pyi") assertTrue("class Editable:" in stub, stub) - assertTrue(" def __init__(self, initialText: str = ..., initialSelection: Span = ...) -> None:" in stub, stub) + assertTrue(" def __init__(self, initial_text: str = ..., initial_selection: Span = ...) -> None:" in stub, stub) assertTrue(" @property\n def text(self) -> str:" in stub, stub) assertTrue(" @property\n def draft(self) -> str | None:" in stub, stub) assertTrue(" replaceAll: _t.ClassVar[" in stub, stub) @@ -168,7 +168,7 @@ class TextStateBindingTest { * A value class the boundary opens (`Meters(value: Double)`, bound as `FLOAT`) is built and returned * as its raw number, so its constructor must stay a module function returning that number: an * `__init__` would promise a `Meters` instance the runtime never gives. Found by - * `:ksp-fixtures:artifact`'s `everyStubbedFunctionIsATableKeyAndEveryTableKeyIsStubbed` reporting + * `:python-multiplatform-gradle-plugin-fixtures:artifact`'s `everyStubbedFunctionIsATableKeyAndEveryTableKeyIsStubbed` reporting * `fixture.valueclass.Meters` as "callable but not stubbed" after the first version of this change. */ @Test diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Deep.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Deep.kt new file mode 100644 index 000000000..75d31df09 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Deep.kt @@ -0,0 +1,8 @@ +// Fixture, not production source: walked as bytecode by `InternalPackageExclusionTest` (issue #192). +@file:Suppress("unused") + +package fixture.artifactinternal.internal.deep + +class HiddenBelowInternal(val x: Int) + +fun hiddenBelowInternalFunction(a: Int): Int = a diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Excluded.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Excluded.kt new file mode 100644 index 000000000..cb74db06c --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Excluded.kt @@ -0,0 +1,8 @@ +// Fixture, not production source: walked as bytecode by `InternalPackageExclusionTest` (issue #192). +@file:Suppress("unused") + +package fixture.artifactinternal.excluded + +class ExcludedByOption(val x: Int) + +fun excludedByOptionFunction(a: Int): Int = a diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Internal.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Internal.kt new file mode 100644 index 000000000..287691821 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Internal.kt @@ -0,0 +1,8 @@ +// Fixture, not production source: walked as bytecode by `InternalPackageExclusionTest` (issue #192). +@file:Suppress("unused") + +package fixture.artifactinternal.internal + +class HiddenByInternal(val x: Int) + +fun hiddenByInternalFunction(a: Int): Int = a diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Lookalike.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Lookalike.kt new file mode 100644 index 000000000..48487bfbb --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Lookalike.kt @@ -0,0 +1,8 @@ +// Fixture, not production source: walked as bytecode by `InternalPackageExclusionTest` (issue #192). +@file:Suppress("unused") + +package fixture.artifactinternal.internals + +class LooksInternal(val x: Int) + +fun looksInternalFunction(a: Int): Int = a diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Normal.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Normal.kt new file mode 100644 index 000000000..45467a838 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/internalpackagefixture/Normal.kt @@ -0,0 +1,8 @@ +// Fixture, not production source: walked as bytecode by `InternalPackageExclusionTest` (issue #192). +@file:Suppress("unused") + +package fixture.artifactinternal + +class Kept(val x: Int) + +fun keptFunction(a: Int): Int = a diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/memberfixture/MemberFixtures.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/memberfixture/MemberFixtures.kt new file mode 100644 index 000000000..6fb08e535 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/memberfixture/MemberFixtures.kt @@ -0,0 +1,61 @@ +// Fixture, not production source: compiled into this module's own test classes and walked as +// bytecode by `MemberFunctionBindingTest` (issue #215), the way `objectfixture` is walked by +// `ObjectMemberBindingTest`. A package of its own, so that tests which pin another fixture +// package's output as an exact list are not disturbed. +package fixture.artifactmember + +/** A class with a constructor, overloaded members, a default argument and the members that must stay out. */ +class Counter(var count: Int) { + fun add(step: Int): Int { + count += step + return count + } + + fun add(step: Int, label: String): String = label + add(step) + + fun reset() { + count = 0 + } + + fun scaled(by: Int = 2): Int = count * by + + suspend fun later(): Int = count + + override fun toString(): String = "Counter($count)" +} + +/** The `kotlinx.coroutines.Job` shape: an interface whose members are all a caller has. */ +interface Handle { + val active: Boolean + + fun stop() + + fun follows(other: Handle): Boolean +} + +fun newHandle(): Handle = object : Handle { + override var active: Boolean = true + + override fun stop() { + active = false + } + + override fun follows(other: Handle): Boolean = other !== this +} + +/** A property and a function of one name: Python has one attribute for both, and the property keeps it. */ +class Sized { + val size: Int = 1 + + fun size(extra: Int): Int = size + extra +} + +/** Generic: its members are not bound yet. */ +class Box(val item: T) { + fun describe(): String = "box" +} + +/** An `object`'s members are #53's and stay bound as statics under the object's name. */ +object Registry { + fun lookup(key: String): Int = key.length +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/samfixture/SamFixtures.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/samfixture/SamFixtures.kt new file mode 100644 index 000000000..7a5ff74e1 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/samfixture/SamFixtures.kt @@ -0,0 +1,40 @@ +// Fixture, not production source: compiled into this module's own test classes and walked as +// bytecode by `SamParameterBindingTest` (issue #216). A package of its own, so that tests which pin +// another fixture package's output as an exact list are not disturbed. +package fixture.artifactsam + +/** A Kotlin `fun interface` with two parameters. */ +fun interface Listener { + fun onEvent(name: String, count: Int) +} + +/** Its method returns a value, which a Python callable cannot stand in for yet (B-5). */ +fun interface Producer { + fun make(): Int +} + +/** Generic: not yet. */ +fun interface Sink { + fun take(item: T) +} + +/** Two abstract methods: an ordinary interface, which crosses as a handle as before. */ +interface NotFunctional { + fun first() + + fun second() +} + +fun runRunnable(block: Runnable) = block.run() + +fun maybeRun(block: Runnable?) { + block?.run() +} + +fun notifyListener(listener: Listener, name: String) = listener.onEvent(name, name.length) + +fun produce(producer: Producer): Int = producer.make() + +fun drain(sink: Sink) = sink.take("item") + +fun keep(handle: NotFunctional) = handle.first() diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/valueclassfixture/ValueClassFixtures.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/valueclassfixture/ValueClassFixtures.kt index 30b77d2f2..59e69a8a7 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/valueclassfixture/ValueClassFixtures.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/artifact/valueclassfixture/ValueClassFixtures.kt @@ -90,7 +90,7 @@ fun Meters.tagged(): String = "meters" * `Function0` has no class file to name, and `DeclarationModelTest` used it for the other half of * `docs/design/pyi-generation-design.md` §2.2's third property. It binds now, so the declined witness moved * to [withSuspendCallback] and this one records the positive: a declaration that **invokes** what it - * is handed, which is what `:ksp-fixtures:artifact` exercises against `kotlin.system.measureTimeMillis` + * is handed, which is what `:python-multiplatform-gradle-plugin-fixtures:artifact` exercises against `kotlin.system.measureTimeMillis` * for real. */ fun withCallback(action: () -> Unit): Int { diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/KotlinNamesOnlyStubTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/KotlinNamesOnlyStubTest.kt index 9269df548..b0bbf8404 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/KotlinNamesOnlyStubTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/KotlinNamesOnlyStubTest.kt @@ -9,12 +9,14 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue /** - * AGENTS.md section 12 rule 1: the binder never exports a Kotlin namespace under another name, and has - * no feature that turns `androidx` into `pythonx`. Pythonic renaming is the `pythonx-compose` - * package's job. + * AGENTS.md section 12 rule 1: the binder never exports a Kotlin **namespace** under another name, and + * has no feature that turns `androidx` into `pythonx`. Member and parameter names are another matter + * (issue #131): the runtime serves each lower-case-first name under its snake_case alias as well, and + * the stub says so -- the Kotlin `def` stays, the alias is an assignment beside it, and a parameter is + * written under the keyword `inspect.signature` shows. * * These tests look at everything the generator can emit for a set of androidx declarations, not at - * one function, so a renaming feature added anywhere in the stub pipeline fails them. + * one function, so a namespace-renaming feature added anywhere in the stub pipeline fails them. */ class KotlinNamesOnlyStubTest { @@ -57,13 +59,34 @@ class KotlinNamesOnlyStubTest { assertFalse(files.keys.any { it.endsWith("py.typed") || it.endsWith("_pm_dispatch.json") }, files.keys.toString()) } + /** The Kotlin name is the `def`; its Pythonic alias is the same object, as the runtime serves it. */ @Test - fun kotlinNamesAreNeverSnakeCased() { + fun aKotlinNameKeepsItsDefAndGainsItsPythonicAlias() { val all = everythingEmitted().values.joinToString("\n") assertTrue("def fillMaxWidth(" in all, all) assertTrue("fraction: float" in all, all) - assertFalse("fill_max_width" in all, all) - assertFalse("window_insets" in all, all) + assertTrue(Regex("^fill_max_width = fillMaxWidth${'$'}", RegexOption.MULTILINE).containsMatchIn(all), all) + assertTrue(Regex("^window_insets_padding = windowInsetsPadding${'$'}", RegexOption.MULTILINE).containsMatchIn(all), all) + // A parameter has one name in a stub: the Pythonic keyword. + assertTrue("window_insets: float" in all, all) + assertFalse("windowInsets: float" in all, all) + // The namespace is not converted: the module path is the Kotlin package. + assertFalse(Regex("^def fill_max_width\\(", RegexOption.MULTILINE).containsMatchIn(all), all) + } + + /** + * The collision policy (`pythonicAliases`, the runtime's `pythonic_aliases`): an alias two Kotlin + * names map to is not written, and an alias that is another declaration's Kotlin name belongs to it. + */ + @Test + fun anAmbiguousAliasIsNotWrittenAndAKotlinNameOwnsItsSpelling() { + val text = renderKotlinFqnStubs( + listOf(decl("toURL"), decl("toUrl"), decl("fooBar"), decl("foo_bar")), + ).values.joinToString("\n") + assertFalse(Regex("^to_url = ", RegexOption.MULTILINE).containsMatchIn(text), text) + assertFalse(Regex("^foo_bar = ", RegexOption.MULTILINE).containsMatchIn(text), text) + assertTrue(Regex("^def foo_bar\\(", RegexOption.MULTILINE).containsMatchIn(text), text) + assertTrue(Regex("^def toURL\\(", RegexOption.MULTILINE).containsMatchIn(text), text) } @Test @@ -78,8 +101,8 @@ class KotlinNamesOnlyStubTest { private fun body(vararg d: DeclarationModel) = renderKotlinFqnStubs(d.toList()).getValue("androidx/compose/foundation/layout/__init__.pyi") - /** The receiver is positional-only; declared parameters are positional-or-keyword by their Kotlin - * name, and a defaulted one carries `= ...`. */ + /** The receiver is positional-only; declared parameters are positional-or-keyword by their + * Pythonic keyword (the Kotlin name where they have none), and a defaulted one carries `= ...`. */ @Test fun declaredParametersAreKeywordCapableByTheirKotlinNameAndDefaultsAreMarked() { val b = body(decl("fillMaxWidth", parameters = listOf(param("fraction", default = true)))) @@ -89,7 +112,7 @@ class KotlinNamesOnlyStubTest { @Test fun aRequiredParameterHasNoDefaultMarker() { val b = body(decl("windowInsetsPadding", parameters = listOf(param("windowInsets")))) - assertTrue("def windowInsetsPadding(receiver: androidx.compose.ui.Modifier, /, windowInsets: float) -> androidx.compose.ui.Modifier:" in b, b) + assertTrue("def windowInsetsPadding(receiver: androidx.compose.ui.Modifier, /, window_insets: float) -> androidx.compose.ui.Modifier:" in b, b) } /** `in` is a Python keyword: it cannot be a keyword argument in `.pyi`, so it and everything diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/MemberFunctionStubTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/MemberFunctionStubTest.kt new file mode 100644 index 000000000..b6c6fd2fe --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/MemberFunctionStubTest.kt @@ -0,0 +1,90 @@ +package python.multiplatform.gradle.stubs + +import python.multiplatform.gradle.model.DeclarationModel +import python.multiplatform.gradle.model.DeclaredParameter +import python.multiplatform.gradle.model.KotlinTypeModel +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Issue #215: a `METHOD` is stubbed where the binding layer serves it -- as a method of its + * receiver's stub class -- and never as a module attribute, the rule #38 gave properties. + * + * Red before the implementation: the renderer treated a `METHOD` row like a function, so + * `fixture.Counter.reset` became `def reset()` in a module `fixture/Counter`, which also takes the + * name `Counter` away from the class. + */ +class MemberFunctionStubTest { + + private val pkg = "fixture" + private val counter = KotlinTypeModel("$pkg.Counter") + private val int = KotlinTypeModel("kotlin.Int") + private val string = KotlinTypeModel("kotlin.String") + private val unit = KotlinTypeModel("kotlin.Unit") + + private val factory = DeclarationModel( + simpleName = "newCounter", + owner = pkg, + ownerIsClass = false, + receiver = null, + parameters = emptyList(), + returnType = counter, + returnBoundaryTag = "OBJECT", + bindingName = "$pkg.newCounter", + ) + + private fun method(binding: String, returns: KotlinTypeModel, tag: String, vararg params: DeclaredParameter) = DeclarationModel( + simpleName = binding.substringAfterLast('.').substringBefore("__"), + owner = "$pkg.Counter", + ownerIsClass = true, + receiver = counter, + receiverBoundaryTag = "OBJECT", + parameters = params.toList(), + returnType = returns, + returnBoundaryTag = tag, + bindingName = binding, + kind = "METHOD", + ) + + private val step = DeclaredParameter("step", int, declaresDefault = false, boundaryTag = "INT") + private val label = DeclaredParameter("label", string, declaresDefault = false, boundaryTag = "STRING") + + private val reset = method("$pkg.Counter.reset", unit, "UNIT") + private val stopNow = method("$pkg.Counter.stopNow", unit, "UNIT") + private val addInt = method("$pkg.Counter.add__Int", int, "INT", step) + private val addIntString = method("$pkg.Counter.add__Int_String", string, "STRING", step, label) + + private fun stub(vararg declarations: DeclarationModel): String { + val files = renderKotlinFqnStubs(listOf(factory) + declarations) + assertEquals(setOf("fixture/__init__.pyi"), files.keys, "a member made a module of its own") + return files.getValue("fixture/__init__.pyi") + } + + @Test + fun aMemberIsAMethodOfItsReceiversClass() { + val stub = stub(reset) + assertTrue("class Counter:" in stub, stub) + assertTrue(" def reset(self) -> None:" in stub, stub) + assertFalse(Regex("^def reset", RegexOption.MULTILINE).containsMatchIn(stub), "a member became a module function") + assertTrue("def newCounter() -> Counter:" in stub, stub) + } + + @Test + fun anOverloadedMemberHasItsExplicitNamesAndAnOverloadedBaseName() { + val stub = stub(addInt, addIntString) + assertTrue(" def add__Int(self, step: int) -> int:" in stub, stub) + assertTrue(" def add__Int_String(self, step: int, label: str) -> str:" in stub, stub) + assertTrue(" @_t.overload\n def add(self, step: int) -> int:" in stub, stub) + assertTrue(" @_t.overload\n def add(self, step: int, label: str) -> str:" in stub, stub) + } + + @Test + fun aMembersPythonicNameIsStubbedBesideItsKotlinName() { + // U-12: the binding layer serves `stop_now` beside `stopNow` on the proxy. + val stub = stub(stopNow) + assertTrue(" def stopNow(self) -> None:" in stub, stub) + assertTrue(" def stop_now(self) -> None:" in stub, stub) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/ObjectStubTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/ObjectStubTest.kt index b12bf797a..ff2736be3 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/ObjectStubTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/ObjectStubTest.kt @@ -85,7 +85,7 @@ class ObjectStubTest { ) val files = renderKotlinFqnStubs(listOf(end, column)) val layout = file(files, pkgLayout) - assertTrue("horizontalAlignment: $alignment.Horizontal = ..." in layout, layout) + assertTrue("horizontal_alignment: $alignment.Horizontal = ..." in layout, layout) assertTrue("import $alignment\n" in layout, layout) } @@ -98,12 +98,14 @@ class ObjectStubTest { } @Test - fun aNestedTypeWhoseNameAnObjectMemberTakesStaysAny() { - // The module already defines `Horizontal` as a constant; a class of that name would shadow it. + fun aNestedTypeWhoseNameAnObjectMemberTakesIsStubbedUnderAPrivateName() { + // The module already defines `Horizontal` as a constant; a class of that name would shadow it, + // so the type is the stub-private `_Horizontal` (issue #217) rather than `Any`. val clash = constant("$alignment.Horizontal", alignmentHorizontal) val text = file(renderKotlinFqnStubs(listOf(clash)), alignment) assertFalse("class Horizontal:" in text, text) - assertTrue("Horizontal: _t.Any" in text, text) + assertTrue("class _Horizontal:" in text, text) + assertTrue("Horizontal: _Horizontal" in text, text) } // ---------------------------------------------------------------- functions of an object diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/PropertyStubTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/PropertyStubTest.kt index 137a326c2..678aae42e 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/PropertyStubTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/PropertyStubTest.kt @@ -102,11 +102,12 @@ class PropertyStubTest { } /** - * A receiver whose name its own module already spends on a function has no stub class to put - * the property in; the key is still accounted for, as a comment, rather than silently absent. + * A receiver whose name its own module already spends on a function gets the stub-private class + * `_Counter` (issue #217), and the property is on it, carrying its key. Before #217 there was no + * class, and the key was accounted for only as a comment. */ @Test - fun aPropertyWhoseReceiverHasNoStubClassIsStillAccountedFor() { + fun aPropertyWhoseReceiverNameAFunctionTakesIsOnTheStubPrivateClass() { val receiver = KotlinTypeModel("p.Counter") val constructorLike = DeclarationModel( simpleName = "Counter", @@ -131,7 +132,10 @@ class PropertyStubTest { kind = "GETTER", ) val stub = renderKotlinFqnStubs(listOf(constructorLike, count)).getValue("p/__init__.pyi") - assertTrue(Regex("^# Kotlin property: p\\.Counter\\.count is not stubbed", RegexOption.MULTILINE).containsMatchIn(stub), stub) + assertTrue("class _Counter:" in stub, stub) + assertTrue(" def count(self) -> int:" in stub, stub) + assertTrue("Kotlin property: p.Counter.count " in stub, stub) + assertFalse("is not stubbed" in stub, stub) assertFalse("class Counter" in stub, stub) } } diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/PythonNameRuleTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/PythonNameRuleTest.kt new file mode 100644 index 000000000..3e6028f72 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/PythonNameRuleTest.kt @@ -0,0 +1,46 @@ +package python.multiplatform.gradle.stubs + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * The stub generator's copy of the naming rule (issue #131) gives the answers the runtime's gives. + * + * The runtime rule is `python_multiplatform.python_name` (`KotlinSurface.kt`), itself + * pythonx-compose 0.1.0a1's; the expected values below are what that Python function returns, and + * `PythonxAdapterTest.theNameRuleConvertsKotlinToPythonAndEitherSpellingReachesOneDeclaration` pins the + * same cases on the runtime side. + */ +class PythonNameRuleTest { + + @Test + fun theForwardRuleMatchesTheRuntime() { + val cases = mapOf( + "fillMaxWidth" to "fill_max_width", + "zIndex" to "z_index", + "toURLString" to "to_url_string", + "getURL" to "get_url", + "size2Dp" to "size2_dp", + "padding__Dp_Dp" to "padding__Dp_Dp", + "paddingFromBaseline__TextUnit" to "padding_from_baseline__TextUnit", + "rememberTextFieldState" to "remember_text_field_state", + "Modifier" to "Modifier", + "URLHandler" to "URLHandler", + "padding" to "padding", + "foo_bar" to "foo_bar", + ) + cases.forEach { (kotlin, python) -> assertEquals(python, pythonName(kotlin), kotlin) } + assertEquals("on_checked_change", snakeCase("onCheckedChange")) + assertEquals("x_url", snakeCase("xURL")) + } + + @Test + fun anAliasIsServedOnlyWhenItIsUnambiguous() { + assertEquals( + mapOf("fill_max_width" to "fillMaxWidth"), + pythonicAliases(listOf("toURL", "toUrl", "fooBar", "foo_bar", "fillMaxWidth", "Modifier", "padding")), + ) + assertEquals(emptyMap(), pythonicAliases(listOf("xURL", "xUrl"), ::snakeCase)) + assertEquals(emptyMap(), pythonicAliases(listOf("onClick", "on_click"), ::snakeCase)) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/TakenNameStubTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/TakenNameStubTest.kt new file mode 100644 index 000000000..e91506dfc --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/TakenNameStubTest.kt @@ -0,0 +1,98 @@ +package python.multiplatform.gradle.stubs + +import python.multiplatform.gradle.model.DeclarationModel +import python.multiplatform.gradle.model.DeclaredParameter +import python.multiplatform.gradle.model.KotlinTypeModel +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Issue #217: a type whose name a same-named factory function takes keeps a stub class. + * + * `kotlinx.coroutines.Job` is an interface and `fun Job(parent: Job? = null): CompletableJob` a + * function, and at run time `kotlinx.coroutines.Job` is the function. The stub used to give the type + * no class at all: the factory returned `Any`, and its properties became "is not stubbed" comments. + * The class is now `_Job`, a name only the stub uses, so `Job` stays the function and the type keeps + * its members. + * + * Red before the implementation: `def Handle() -> _t.Any`, no `class _Handle`, and a `Kotlin property: + * ... is not stubbed` comment. + */ +class TakenNameStubTest { + + private val pkg = "fixture" + private val handle = KotlinTypeModel("$pkg.Handle") + private val bool = KotlinTypeModel("kotlin.Boolean") + + private val factory = DeclarationModel( + simpleName = "Handle", + owner = pkg, + ownerIsClass = false, + receiver = null, + parameters = emptyList(), + returnType = handle, + returnBoundaryTag = "OBJECT", + bindingName = "$pkg.Handle", + ) + + private val use = DeclarationModel( + simpleName = "use", + owner = pkg, + ownerIsClass = false, + receiver = null, + parameters = listOf(DeclaredParameter("handle", handle, declaresDefault = false, boundaryTag = "OBJECT")), + returnType = KotlinTypeModel("kotlin.Unit"), + returnBoundaryTag = "UNIT", + bindingName = "$pkg.use", + ) + + private val active = DeclarationModel( + simpleName = "active", + owner = "$pkg.Handle", + ownerIsClass = true, + receiver = handle, + receiverBoundaryTag = "OBJECT", + parameters = emptyList(), + returnType = bool, + returnBoundaryTag = "BOOLEAN", + bindingName = "$pkg.Handle.active", + kind = "GETTER", + ) + + private val stop = DeclarationModel( + simpleName = "stop", + owner = "$pkg.Handle", + ownerIsClass = true, + receiver = handle, + receiverBoundaryTag = "OBJECT", + parameters = emptyList(), + returnType = KotlinTypeModel("kotlin.Unit"), + returnBoundaryTag = "UNIT", + bindingName = "$pkg.Handle.stop", + kind = "METHOD", + ) + + private fun stub() = renderKotlinFqnStubs(listOf(factory, use, active, stop)).getValue("fixture/__init__.pyi") + + @Test + fun theFunctionKeepsTheNameAndReturnsTheStubPrivateClass() { + val stub = stub() + assertTrue("def Handle() -> _Handle:" in stub, stub) + assertFalse(Regex("^class Handle\\b", RegexOption.MULTILINE).containsMatchIn(stub), "a class took the function's name") + } + + @Test + fun theTypeKeepsItsPropertiesAndMembers() { + val stub = stub() + assertTrue("class _Handle:" in stub, stub) + assertTrue(" def active(self) -> bool:" in stub, stub) + assertTrue(" def stop(self) -> None:" in stub, stub) + assertFalse("is not stubbed" in stub, stub) + } + + @Test + fun everyUseOfTheTypeIsTheStubPrivateClass() { + assertTrue("def use(handle: _Handle) -> None:" in stub(), stub()) + } +} diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/TypedStubTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/TypedStubTest.kt index 10e0a811a..964bf9651 100644 --- a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/TypedStubTest.kt +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/TypedStubTest.kt @@ -146,7 +146,8 @@ class TypedStubTest { assertTrue(order[0].contains("all: androidx.compose.ui.unit.Dp | float"), order.toString()) assertTrue(order[1].contains("horizontal"), order.toString()) assertTrue(order[2].contains("start"), order.toString()) - assertTrue(order[3].contains("paddingValues"), order.toString()) + // Issue #131: the parameter is written under the keyword `inspect.signature` shows. + assertTrue(order[3].contains("padding_values"), order.toString()) // ... and the explicit table-key spellings are still there, one def each. assertTrue("def padding__Dp(receiver: androidx.compose.ui.Modifier, /, all:" in layout, layout) assertTrue("def padding__PaddingValues(" in layout, layout) @@ -154,7 +155,7 @@ class TypedStubTest { val ui = file(files, pkgUi) val methodOrder = Regex("""class _Modifier_padding\(_t\.Protocol\):\n((?: .*\n)+)""").find(ui)!!.groupValues[1] val calls = Regex("""def __call__\(self, (\w+)""").findAll(methodOrder).map { it.groupValues[1] }.toList() - assertEquals(listOf("all", "horizontal", "start", "paddingValues"), calls, ui) + assertEquals(listOf("all", "horizontal", "start", "padding_values"), calls, ui) } /** Python's `int`/`float` cannot tell two Kotlin overloads apart, so a checker calls every later @@ -245,7 +246,7 @@ class TypedStubTest { ) val body = file(files(checkbox), "androidx.compose.material3") assertTrue( - "def Checkbox(checked: bool, onCheckedChange: _t.Callable[[bool], None] | None, " + + "def Checkbox(checked: bool, on_checked_change: _t.Callable[[bool], None] | None, " + "modifier: androidx.compose.ui.Modifier = ..., *, content: _t.Callable[[], None]) -> None:" in body, body, ) @@ -271,27 +272,41 @@ class TypedStubTest { // ---------------------------------------------------------------------- name collisions /** A Kotlin factory function and the class it makes share a name (`PaddingValues(...)`). One - * Python module cannot define both, and the runtime resolves that name to the function. */ + * Python module cannot define both, and the runtime resolves that name to the function, so the + * class is the stub-private `_PaddingValues` (issue #217) -- not `Any`, as it was before. */ @Test - fun aClassWhoseNameIsAFunctionInItsModuleFallsBackToAny() { + fun aClassWhoseNameIsAFunctionInItsModuleIsStubbedUnderAPrivateName() { val factory = DeclarationModel( simpleName = "PaddingValues", owner = pkgLayout, ownerIsClass = false, receiver = null, parameters = listOf(p("all", dp, "FLOAT")), returnType = paddingValues, returnBoundaryTag = "OBJECT", bindingName = "$pkgLayout.PaddingValues", ) val layout = file(files(factory), pkgLayout) - assertTrue("def PaddingValues(all: androidx.compose.ui.unit.Dp | float) -> _t.Any:" in layout, layout) + assertTrue("def PaddingValues(all: androidx.compose.ui.unit.Dp | float) -> _PaddingValues:" in layout, layout) assertFalse("class PaddingValues" in layout, layout) + assertTrue("class _PaddingValues:" in layout, layout) } // ------------------------------------------------------------------------ Kotlin names only + /** + * Issue #131: the Kotlin names stay and the Pythonic aliases are added beside them -- as + * `alias = kotlinName` at module level and as a second `ClassVar` of the same Protocol on the + * receiver -- and nothing is emitted under `pythonx`. + */ @Test - fun theTypedStubsStillCarryOnlyKotlinNames() { + fun theTypedStubsCarryTheKotlinNamesAndThePythonicAliasesAndNothingUnderPythonx() { val all = files(*padding.toTypedArray(), fillMaxWidth) assertEquals(emptyList(), all.keys.filter { it.startsWith("pythonx/") }) val text = all.keys.joinToString("\n") + all.values.joinToString("\n") assertFalse("pythonx" in text, text) - assertFalse("fill_max_width" in text || "padding_values" in text, text) + val layout = file(all, pkgLayout) + assertTrue(Regex("^def fillMaxWidth\\(", RegexOption.MULTILINE).containsMatchIn(layout), layout) + assertTrue(Regex("^fill_max_width = fillMaxWidth${'$'}", RegexOption.MULTILINE).containsMatchIn(layout), layout) + val ui = file(all, pkgUi) + assertTrue(" fillMaxWidth: _t.ClassVar[_Modifier_fillMaxWidth]" in ui, ui) + assertTrue(" fill_max_width: _t.ClassVar[_Modifier_fillMaxWidth]" in ui, ui) + // `padding` has no other spelling: nothing is invented for it. + assertFalse(Regex("^padding_\\w* = ", RegexOption.MULTILINE).containsMatchIn(layout), layout) } } diff --git a/python-multiplatform-ksp/build.gradle.kts b/python-multiplatform-ksp/build.gradle.kts index a171a8475..67b3f22ac 100644 --- a/python-multiplatform-ksp/build.gradle.kts +++ b/python-multiplatform-ksp/build.gradle.kts @@ -21,10 +21,30 @@ tasks.test { // python-multiplatform-gradle-plugin's `generateCoordinates` task, from this same group/artifactId/ // version) names an artifact that never gets published anywhere -- every consumer outside this repo // resolves the plugin id fine and then fails on the processor dependency itself. +// The TypedPython gate, published beside the processor as `:typedpython-gate@zip` (#20, +// toolchain#90): the typedpython wheel under its real file name plus typedpython-gate.json (wheel +// sha256, pyrefly version and the sha256 of every pyrefly wheel), so toolchain's typedpythonCheck +// installs it with uv --require-hashes and needs no -Ptypedpython.wheelDir. gate_bundle.py builds it. +val typedpythonGateBundle by tasks.registering(Exec::class) { + group = "build" + description = "Builds the TypedPython gate bundle (wheel + manifest) published with this module." + val out = layout.buildDirectory.file("typedpython-gate/typedpython-gate.zip") + inputs.files(fileTree("src/main/python") { exclude("**/__pycache__/**") }).withPropertyName("gateSources") + inputs.files("pyproject.toml", "typedpython-gate.lock.json", "gate_bundle.py").withPropertyName("gateBuild") + outputs.file(out) + executable = "python3" + args("gate_bundle.py", "--out", out.get().asFile.absolutePath) +} + publishing { publications { create("maven") { from(components["java"]) + artifact(typedpythonGateBundle.map { it.outputs.files.singleFile }) { + classifier = "typedpython-gate" + extension = "zip" + builtBy(typedpythonGateBundle) + } } } } diff --git a/ksp-fixtures/android/build.gradle.kts b/python-multiplatform-ksp/fixtures/android/build.gradle.kts similarity index 87% rename from ksp-fixtures/android/build.gradle.kts rename to python-multiplatform-ksp/fixtures/android/build.gradle.kts index b1f642744..61eb70d21 100644 --- a/ksp-fixtures/android/build.gradle.kts +++ b/python-multiplatform-ksp/fixtures/android/build.gradle.kts @@ -11,10 +11,10 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget * java.lang.NoSuchMethodError: 'void com.android.build.api.variant * .AndroidComponentsExtension.addKspConfigurations(boolean)' * - * in every module carrying an Android plugin. Neither `:ksp-fixtures:library` nor - * `:ksp-fixtures:app` applies one, so both stayed green while every Android consumer of the + * in every module carrying an Android plugin. Neither `:python-multiplatform-ksp:fixtures:library` nor + * `:python-multiplatform-ksp:fixtures:app` applies one, so both stayed green while every Android consumer of the * plugin was broken. **This module exists to fail in that situation**: if the AGP/KSP pair ever - * drifts back apart, `gradlew :ksp-fixtures:android:testDebugUnitTest` stops configuring at all. + * drifts back apart, `gradlew :python-multiplatform-ksp:fixtures:android:testDebugUnitTest` stops configuring at all. * * It paid for itself on its first run by failing for a *second* reason: AGP names its KSP * configurations with the build type last (`kspAndroidTestDebug`), so the plugin's @@ -79,7 +79,7 @@ android { pythonBindings { // `com.android.library` is not `com.android.application`, so the aggregating role is not - // inferred -- stated for the same reason `:ksp-fixtures:app` states it. Aggregating is what + // inferred -- stated for the same reason `:python-multiplatform-ksp:fixtures:app` states it. Aggregating is what // puts a `FunctionTable` in front of the test below; a `library` role would only emit a // fragment, and the test would be asserting on the generator rather than on the table. role.set("app") diff --git a/ksp-fixtures/android/src/androidInstrumentedTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt b/python-multiplatform-ksp/fixtures/android/src/androidInstrumentedTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt similarity index 93% rename from ksp-fixtures/android/src/androidInstrumentedTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt rename to python-multiplatform-ksp/fixtures/android/src/androidInstrumentedTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt index 813f13200..fa4e90300 100644 --- a/ksp-fixtures/android/src/androidInstrumentedTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt +++ b/python-multiplatform-ksp/fixtures/android/src/androidInstrumentedTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue /** - * On-device instrumented execution of ksp-fixtures:android upcall table tests. + * On-device instrumented execution of python-multiplatform-ksp:fixtures:android upcall table tests. */ class GeneratedAndroidTableTest { @@ -31,7 +31,7 @@ class GeneratedAndroidTableTest { @Test fun theAndroidModulesFragmentIsInTheAggregatedTable() { - assertEquals(setOf("io_github_thisisthepy_ksp_fixtures_android"), UpcallTable.moduleNames) + assertEquals(setOf("io_github_thisisthepy_python_multiplatform_ksp_fixtures_android"), UpcallTable.moduleNames) assertTrue(UpcallTable.resolve("fixture.android.androidDouble").isValid) } @@ -115,7 +115,7 @@ class GeneratedAndroidTableTest { val count = installAndCountEntries() assertTrue(count > 0) - assertEquals(setOf("io_github_thisisthepy_ksp_fixtures_android"), UpcallTable.moduleNames) + assertEquals(setOf("io_github_thisisthepy_python_multiplatform_ksp_fixtures_android"), UpcallTable.moduleNames) assertTrue(UpcallTable.resolve("fixture.android.androidDouble").isValid) assertFalse( UpcallTable.resolve("fixture.android.installGeneratedUpcallTable").isValid, diff --git a/ksp-fixtures/android/src/androidMain/kotlin/fixture/android/AndroidFunctions.kt b/python-multiplatform-ksp/fixtures/android/src/androidMain/kotlin/fixture/android/AndroidFunctions.kt similarity index 84% rename from ksp-fixtures/android/src/androidMain/kotlin/fixture/android/AndroidFunctions.kt rename to python-multiplatform-ksp/fixtures/android/src/androidMain/kotlin/fixture/android/AndroidFunctions.kt index a4fc0d6f4..eef121880 100644 --- a/ksp-fixtures/android/src/androidMain/kotlin/fixture/android/AndroidFunctions.kt +++ b/python-multiplatform-ksp/fixtures/android/src/androidMain/kotlin/fixture/android/AndroidFunctions.kt @@ -5,7 +5,7 @@ import python.multiplatform.reflection.PythonInternal /** * The Python-facing surface of the Android fixture. * - * Deliberately the same shapes `ksp-fixtures/library` uses on desktop -- a top-level function, a + * Deliberately the same shapes `python-multiplatform-ksp/fixtures/library` uses on desktop -- a top-level function, a * narrowing `Int` signature, a `Unit` return, a class with a method and a mutable property, and * one `@PythonInternal` exclusion. The point is not to re-test the generator's policy (the * desktop fixture does that against a far bigger surface) but to prove the *same* generator @@ -26,7 +26,7 @@ fun androidHidden(): String = "must not appear in the generated table" class AndroidCounter(var count: Long = 0) { /** ROADMAP §13's second defect, on the AGP side of the generator: `isMutable` is true and the - * setter is not something generated code may name. `ksp-fixtures/library` pins all three + * setter is not something generated code may name. `python-multiplatform-ksp/fixtures/library` pins all three * restricted visibilities; this one only checks the same decision is made over here. */ var lastLabel: String = "" private set diff --git a/ksp-fixtures/android/src/androidUnitTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt b/python-multiplatform-ksp/fixtures/android/src/androidUnitTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt similarity index 96% rename from ksp-fixtures/android/src/androidUnitTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt rename to python-multiplatform-ksp/fixtures/android/src/androidUnitTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt index 26c1f121c..3876b6492 100644 --- a/ksp-fixtures/android/src/androidUnitTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt +++ b/python-multiplatform-ksp/fixtures/android/src/androidUnitTest/kotlin/fixture/android/GeneratedAndroidTableTest.kt @@ -44,7 +44,7 @@ class GeneratedAndroidTableTest { // The name the gradle plugin derives from this module's group and path. Its presence is // what says KSP ran here at all -- the state ROADMAP §13 describes has no fragment // because the build never got past configuration. - assertEquals(setOf("io_github_thisisthepy_ksp_fixtures_android"), UpcallTable.moduleNames) + assertEquals(setOf("io_github_thisisthepy_python_multiplatform_ksp_fixtures_android"), UpcallTable.moduleNames) assertTrue(UpcallTable.resolve("fixture.android.androidDouble").isValid) } @@ -132,7 +132,7 @@ class GeneratedAndroidTableTest { val count = installAndCountEntries() assertTrue(count > 0) - assertEquals(setOf("io_github_thisisthepy_ksp_fixtures_android"), UpcallTable.moduleNames) + assertEquals(setOf("io_github_thisisthepy_python_multiplatform_ksp_fixtures_android"), UpcallTable.moduleNames) assertTrue(UpcallTable.resolve("fixture.android.androidDouble").isValid) assertFalse( UpcallTable.resolve("fixture.android.installGeneratedUpcallTable").isValid, diff --git a/ksp-fixtures/android/src/commonMain/kotlin/fixture/android/TableInstall.kt b/python-multiplatform-ksp/fixtures/android/src/commonMain/kotlin/fixture/android/TableInstall.kt similarity index 92% rename from ksp-fixtures/android/src/commonMain/kotlin/fixture/android/TableInstall.kt rename to python-multiplatform-ksp/fixtures/android/src/commonMain/kotlin/fixture/android/TableInstall.kt index ac3370ec7..a01c677dc 100644 --- a/ksp-fixtures/android/src/commonMain/kotlin/fixture/android/TableInstall.kt +++ b/python-multiplatform-ksp/fixtures/android/src/commonMain/kotlin/fixture/android/TableInstall.kt @@ -13,7 +13,7 @@ import python.multiplatform.reflection.UpcallTable * exactly one. * * Nothing shared code writes should have to know any of that, which is what this file asserts by - * being identical in shape to `ksp-fixtures/app`'s. + * being identical in shape to `python-multiplatform-ksp/fixtures/app`'s. */ @InstallsUpcallTable expect fun installGeneratedUpcallTable() diff --git a/ksp-fixtures/app/build.gradle.kts b/python-multiplatform-ksp/fixtures/app/build.gradle.kts similarity index 99% rename from ksp-fixtures/app/build.gradle.kts rename to python-multiplatform-ksp/fixtures/app/build.gradle.kts index 8f136bb14..8404411cd 100644 --- a/ksp-fixtures/app/build.gradle.kts +++ b/python-multiplatform-ksp/fixtures/app/build.gradle.kts @@ -40,7 +40,7 @@ kotlin { val commonMain by getting { dependencies { implementation(projects.pythonMultiplatform) - implementation(projects.kspFixtures.library) + implementation(projects.pythonMultiplatformKsp.fixtures.library) } } val desktopTest by getting { diff --git a/ksp-fixtures/app/src/androidNativeArm64Test/kotlin/fixture/app/NativeSmokeTest.kt b/python-multiplatform-ksp/fixtures/app/src/androidNativeArm64Test/kotlin/fixture/app/NativeSmokeTest.kt similarity index 100% rename from ksp-fixtures/app/src/androidNativeArm64Test/kotlin/fixture/app/NativeSmokeTest.kt rename to python-multiplatform-ksp/fixtures/app/src/androidNativeArm64Test/kotlin/fixture/app/NativeSmokeTest.kt diff --git a/ksp-fixtures/app/src/androidNativeMain/kotlin/fixture/app/NativeInstall.kt b/python-multiplatform-ksp/fixtures/app/src/androidNativeMain/kotlin/fixture/app/NativeInstall.kt similarity index 87% rename from ksp-fixtures/app/src/androidNativeMain/kotlin/fixture/app/NativeInstall.kt rename to python-multiplatform-ksp/fixtures/app/src/androidNativeMain/kotlin/fixture/app/NativeInstall.kt index 85adc2b61..77625f03a 100644 --- a/ksp-fixtures/app/src/androidNativeMain/kotlin/fixture/app/NativeInstall.kt +++ b/python-multiplatform-ksp/fixtures/app/src/androidNativeMain/kotlin/fixture/app/NativeInstall.kt @@ -10,6 +10,6 @@ package fixture.app * found. Before the seam, the only way out was one hand-written `actual` per leaf. * * That it compiles for both leaves is the whole assertion; there is no test binary here. - * Verified with `:ksp-fixtures:app:compileKotlinAndroidNativeArm64` and `...X64`. + * Verified with `:python-multiplatform-ksp:fixtures:app:compileKotlinAndroidNativeArm64` and `...X64`. */ fun installOnNative(): String = installAndDescribeTable() diff --git a/ksp-fixtures/app/src/commonMain/kotlin/fixture/app/AppExtras.kt b/python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/AppExtras.kt similarity index 100% rename from ksp-fixtures/app/src/commonMain/kotlin/fixture/app/AppExtras.kt rename to python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/AppExtras.kt diff --git a/ksp-fixtures/app/src/commonMain/kotlin/fixture/app/AppFunctions.kt b/python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/AppFunctions.kt similarity index 100% rename from ksp-fixtures/app/src/commonMain/kotlin/fixture/app/AppFunctions.kt rename to python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/AppFunctions.kt diff --git a/ksp-fixtures/app/src/commonMain/kotlin/fixture/app/ArgumentRelease.kt b/python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/ArgumentRelease.kt similarity index 100% rename from ksp-fixtures/app/src/commonMain/kotlin/fixture/app/ArgumentRelease.kt rename to python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/ArgumentRelease.kt diff --git a/ksp-fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt b/python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt similarity index 80% rename from ksp-fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt rename to python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt index 0c28391a4..0a9cfbfc7 100644 --- a/ksp-fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt +++ b/python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt @@ -10,15 +10,15 @@ import python.multiplatform.reflection.UpcallTable * compilation, so this file -- `commonMain`, which every leaf depends on -- cannot name it. * Written the obvious way, the fixture does not compile: * - * e: ksp-fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt:4:39 + * e: python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/TableInstall.kt:4:39 * Unresolved reference 'FunctionTable'. * - * observed on `:ksp-fixtures:app:compileKotlinDesktop`. No fixture had this shape before, which + * observed on `:python-multiplatform-ksp:fixtures:app:compileKotlinDesktop`. No fixture had this shape before, which * is why the whole defect only turned up in `sample`: both existing fixtures reached the table * from `desktopTest`, a *leaf* compilation, where it resolves fine. * * The seam: one `expect` here, and `python-multiplatform-ksp` emits the `actual` into every leaf - * it generates a `FunctionTable` for. `:ksp-fixtures:app` has three leaves (`desktop`, + * it generates a `FunctionTable` for. `:python-multiplatform-ksp:fixtures:app` has three leaves (`desktop`, * `androidNativeArm64`, `androidNativeX64`) and not one of them needs a hand-written line. */ @InstallsUpcallTable diff --git a/ksp-fixtures/app/src/commonMain/kotlin/fixture/app/TypedPythonBridge.kt b/python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/TypedPythonBridge.kt similarity index 100% rename from ksp-fixtures/app/src/commonMain/kotlin/fixture/app/TypedPythonBridge.kt rename to python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/TypedPythonBridge.kt diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/CommonInstallTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/CommonInstallTest.kt similarity index 95% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/CommonInstallTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/CommonInstallTest.kt index e7db0e188..e30de7b5b 100644 --- a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/CommonInstallTest.kt +++ b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/CommonInstallTest.kt @@ -38,7 +38,7 @@ class CommonInstallTest { assertTrue(UpcallTable.callableCount > 0, "commonMain installed nothing: $summary") assertEquals( - setOf("io_github_thisisthepy_ksp_fixtures_library", "io_github_thisisthepy_ksp_fixtures_app"), + setOf("io_github_thisisthepy_python_multiplatform_ksp_fixtures_library", "io_github_thisisthepy_python_multiplatform_ksp_fixtures_app"), UpcallTable.moduleNames, "the seam must install the same aggregate FunctionTable.fragments does", ) diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedDeclarationKindsTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedDeclarationKindsTest.kt similarity index 99% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedDeclarationKindsTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedDeclarationKindsTest.kt index abe21e012..d30ab0784 100644 --- a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedDeclarationKindsTest.kt +++ b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedDeclarationKindsTest.kt @@ -23,7 +23,7 @@ import kotlin.test.assertTrue /** * ROADMAP §7's "companion-object members, interfaces, enums and annotation classes are not - * exposed", closed against the table KSP generated from `ksp-fixtures/library` -- not against a + * exposed", closed against the table KSP generated from `python-multiplatform-ksp/fixtures/library` -- not against a * hand-written fragment. * * Two of these tests assert an *absence* (annotation classes, generics). They are not diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedStaticPropertyProxyTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedStaticPropertyProxyTest.kt similarity index 100% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedStaticPropertyProxyTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedStaticPropertyProxyTest.kt diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedSuspendTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedSuspendTest.kt similarity index 99% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedSuspendTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedSuspendTest.kt index 49ba2299f..be5a1b147 100644 --- a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedSuspendTest.kt +++ b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedSuspendTest.kt @@ -21,7 +21,7 @@ import kotlin.test.assertTrue /** * What the generator does with `suspend`, pinned against the table KSP actually produced from - * `ksp-fixtures/library/.../Suspending.kt`. + * `python-multiplatform-ksp/fixtures/library/.../Suspending.kt`. * * The answer used to be **silently skipped** -- `BindingPolicy.isExposedFunctionShape` dropped any * declaration carrying `Modifier.SUSPEND`, so nothing was emitted and nothing was logged, and a diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedTableTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedTableTest.kt similarity index 96% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedTableTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedTableTest.kt index 1be5539ff..b3ec843dd 100644 --- a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedTableTest.kt +++ b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/GeneratedTableTest.kt @@ -21,7 +21,7 @@ import kotlin.test.assertTrue * This is the whole point of ROADMAP §7's generator half: everything * `python.multiplatform.reflection.UpcallTableTest` proves against hand-written fragments in * `TestFragments.kt`, proved here against a `FunctionTable` that KSP actually generated from - * `ksp-fixtures/library`'s and this module's own sources -- no hand-written fragment anywhere in + * `python-multiplatform-ksp/fixtures/library`'s and this module's own sources -- no hand-written fragment anywhere in * this file. */ class GeneratedTableTest { @@ -46,7 +46,7 @@ class GeneratedTableTest { // group and path -- neither fixture states a module name any more, and the group is in // there because every fragment object in every artifact shares one package. assertEquals( - setOf("io_github_thisisthepy_ksp_fixtures_library", "io_github_thisisthepy_ksp_fixtures_app"), + setOf("io_github_thisisthepy_python_multiplatform_ksp_fixtures_library", "io_github_thisisthepy_python_multiplatform_ksp_fixtures_app"), UpcallTable.moduleNames, ) assertTrue(UpcallTable.resolve("fixture.library.add").isValid) diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/KspClassResultProxyTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/KspClassResultProxyTest.kt similarity index 100% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/KspClassResultProxyTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/KspClassResultProxyTest.kt diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/ProxyObjectArgumentTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/ProxyObjectArgumentTest.kt similarity index 100% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/ProxyObjectArgumentTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/ProxyObjectArgumentTest.kt diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/PyObjectArgumentReleaseTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/PyObjectArgumentReleaseTest.kt similarity index 100% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/PyObjectArgumentReleaseTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/PyObjectArgumentReleaseTest.kt diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/RefHolderCycleCollectionTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/RefHolderCycleCollectionTest.kt similarity index 100% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/RefHolderCycleCollectionTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/RefHolderCycleCollectionTest.kt diff --git a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/TypedPythonKotlinCallTest.kt b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/TypedPythonKotlinCallTest.kt similarity index 99% rename from ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/TypedPythonKotlinCallTest.kt rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/TypedPythonKotlinCallTest.kt index 805106f9b..101d0d7a6 100644 --- a/ksp-fixtures/app/src/desktopTest/kotlin/fixture/app/TypedPythonKotlinCallTest.kt +++ b/python-multiplatform-ksp/fixtures/app/src/desktopTest/kotlin/fixture/app/TypedPythonKotlinCallTest.kt @@ -52,7 +52,7 @@ class TypedPythonKotlinCallTest { val propertiesPath = System.getProperty("typedpython.bridge.properties") assumeTrue( "TypedPython bridge extension not built: system property typedpython.bridge.properties is unset " + - "(run through :ksp-fixtures:app:desktopTest)", + "(run through :python-multiplatform-ksp:fixtures:app:desktopTest)", propertiesPath != null, ) val file = File(propertiesPath!!) diff --git a/ksp-fixtures/app/src/desktopTest/typedpython/fixture/__init__.pyi b/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/fixture/__init__.pyi similarity index 100% rename from ksp-fixtures/app/src/desktopTest/typedpython/fixture/__init__.pyi rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/fixture/__init__.pyi diff --git a/ksp-fixtures/app/src/desktopTest/typedpython/fixture/app/__init__.pyi b/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/fixture/app/__init__.pyi similarity index 86% rename from ksp-fixtures/app/src/desktopTest/typedpython/fixture/app/__init__.pyi rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/fixture/app/__init__.pyi index 267595828..6e9f44cac 100644 --- a/ksp-fixtures/app/src/desktopTest/typedpython/fixture/app/__init__.pyi +++ b/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/fixture/app/__init__.pyi @@ -1,5 +1,5 @@ # Type-checking stub for the TypedPython gate only (issue #45). It mirrors the part of -# `ksp-fixtures/app/src/commonMain/kotlin/fixture/app/TypedPythonBridge.kt` that +# `python-multiplatform-ksp/fixtures/app/src/commonMain/kotlin/fixture/app/TypedPythonBridge.kt` that # `tp_kotlin_bridge.py` uses. Kotlin `Long` is `int`, `Double` is `float`, `PyObject?` is `object`. # If this drifts from the Kotlin, the gate still passes and the run-time test fails at the call. diff --git a/ksp-fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py b/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py similarity index 100% rename from ksp-fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py rename to python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py diff --git a/ksp-fixtures/app/typedpython/build_bridge.py b/python-multiplatform-ksp/fixtures/app/typedpython/build_bridge.py similarity index 97% rename from ksp-fixtures/app/typedpython/build_bridge.py rename to python-multiplatform-ksp/fixtures/app/typedpython/build_bridge.py index cd754b3e6..d1cbeb698 100644 --- a/ksp-fixtures/app/typedpython/build_bridge.py +++ b/python-multiplatform-ksp/fixtures/app/typedpython/build_bridge.py @@ -1,6 +1,6 @@ """Builds `tp_kotlin_bridge.py` into a TypedPython extension for the CPython this fixture embeds (#45). -Run by the `buildTypedPythonBridge` task in `ksp-fixtures/app/build.gradle.kts` with a *host* +Run by the `buildTypedPythonBridge` task in `python-multiplatform-ksp/fixtures/app/build.gradle.kts` with a *host* interpreter that has Pyrefly 1.3.2 (the gate needs it; `-PtypedpythonPython`, default `python-multiplatform-ksp/.venv/bin/python`). The host interpreter only runs the compiler; the extension is built for the embedded one: diff --git a/ksp-fixtures/library/build.gradle.kts b/python-multiplatform-ksp/fixtures/library/build.gradle.kts similarity index 93% rename from ksp-fixtures/library/build.gradle.kts rename to python-multiplatform-ksp/fixtures/library/build.gradle.kts index 4b52c23bd..417ec00b7 100644 --- a/ksp-fixtures/library/build.gradle.kts +++ b/python-multiplatform-ksp/fixtures/library/build.gradle.kts @@ -20,7 +20,7 @@ kotlin { // (docs/design/upcall.md §2.9's open question). Compile-only here: androidNativeArm64 // targets a device/emulator this workspace does not run tests against. androidNativeArm64() - // `:ksp-fixtures:app` grew a second Native leaf so that it would have an intermediate + // `:python-multiplatform-ksp:fixtures:app` grew a second Native leaf so that it would have an intermediate // `androidNativeMain` -- ROADMAP §13's `iosMain` shape, without Xcode. A consumer's targets // have to be a subset of its dependency's, so this one follows or the app module fails // dependency resolution ("Unresolved platforms: [androidNativeX64]") before compiling. diff --git a/ksp-fixtures/library/src/commonMain/kotlin/fixture/library/Declarations.kt b/python-multiplatform-ksp/fixtures/library/src/commonMain/kotlin/fixture/library/Declarations.kt similarity index 97% rename from ksp-fixtures/library/src/commonMain/kotlin/fixture/library/Declarations.kt rename to python-multiplatform-ksp/fixtures/library/src/commonMain/kotlin/fixture/library/Declarations.kt index 75a27a9c1..94456be08 100644 --- a/ksp-fixtures/library/src/commonMain/kotlin/fixture/library/Declarations.kt +++ b/python-multiplatform-ksp/fixtures/library/src/commonMain/kotlin/fixture/library/Declarations.kt @@ -8,7 +8,7 @@ import python.multiplatform.reflection.PythonInternal * before the generator learned about them (generics, abstract classes). * * Everything here exists to be asserted against in - * `ksp-fixtures/app/src/desktopTest/.../GeneratedDeclarationKindsTest.kt`, against the table KSP + * `python-multiplatform-ksp/fixtures/app/src/desktopTest/.../GeneratedDeclarationKindsTest.kt`, against the table KSP * actually generated rather than a hand-written fragment. */ diff --git a/ksp-fixtures/library/src/commonMain/kotlin/fixture/library/LibFunctions.kt b/python-multiplatform-ksp/fixtures/library/src/commonMain/kotlin/fixture/library/LibFunctions.kt similarity index 100% rename from ksp-fixtures/library/src/commonMain/kotlin/fixture/library/LibFunctions.kt rename to python-multiplatform-ksp/fixtures/library/src/commonMain/kotlin/fixture/library/LibFunctions.kt diff --git a/ksp-fixtures/library/src/commonMain/kotlin/fixture/library/Suspending.kt b/python-multiplatform-ksp/fixtures/library/src/commonMain/kotlin/fixture/library/Suspending.kt similarity index 100% rename from ksp-fixtures/library/src/commonMain/kotlin/fixture/library/Suspending.kt rename to python-multiplatform-ksp/fixtures/library/src/commonMain/kotlin/fixture/library/Suspending.kt diff --git a/python-multiplatform-ksp/gate_bundle.py b/python-multiplatform-ksp/gate_bundle.py new file mode 100644 index 000000000..7f28bd5e6 --- /dev/null +++ b/python-multiplatform-ksp/gate_bundle.py @@ -0,0 +1,76 @@ +"""Build the TypedPython gate bundle that ships beside the KSP processor (#20, toolchain#90). + + python3 gate_bundle.py --out /typedpython-gate.zip + +The zip holds, at its root, the `typedpython` wheel under its real file name (so uv's +`--find-links ` finds it) and `typedpython-gate.json`: + + {"format": 1, + "typedpython": {"version": ..., "wheel": ..., "sha256": ...}, + "pyrefly": {"version": ..., "sha256": {: , ...}}} + +The pyrefly hashes come from `typedpython-gate.lock.json` (PyPI's published digests for every pyrefly +wheel of the pinned version), so toolchain can install pyrefly from PyPI with `--require-hashes`. +The wheel is built with `uv build`; zip entries carry a fixed timestamp so the bundle depends only +on its inputs. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import subprocess +import sys +import tempfile +import tomllib +import zipfile +from pathlib import Path + +MODULE = Path(__file__).resolve().parent +LOCK = MODULE / "typedpython-gate.lock.json" +MANIFEST = "typedpython-gate.json" +FIXED_TIME = (1980, 1, 1, 0, 0, 0) + + +def build(out: Path) -> Path: + project = tomllib.loads((MODULE / "pyproject.toml").read_text())["project"] + version = project["version"] + lock = json.loads(LOCK.read_text())["pyrefly"] + if f"pyrefly=={lock['version']}" not in project["dependencies"]: + sys.exit(f"{LOCK.name} pins pyrefly {lock['version']}, pyproject.toml depends on {project['dependencies']}") + out.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=out.parent) as tmp: + run = subprocess.run(["uv", "build", "--wheel", "--out-dir", tmp, str(MODULE)], + capture_output=True, text=True) + if run.returncode != 0: + sys.exit(f"uv build failed ({run.returncode}):\n{run.stdout}{run.stderr}") + wheel = Path(tmp) / f"typedpython-{version}-py3-none-any.whl" + if not wheel.is_file(): + sys.exit(f"expected {wheel.name}, uv built {sorted(p.name for p in Path(tmp).iterdir())}") + data = wheel.read_bytes() + manifest = { + "format": 1, + "typedpython": {"version": version, "wheel": wheel.name, "sha256": hashlib.sha256(data).hexdigest()}, + "pyrefly": {"version": lock["version"], "sha256": lock["sha256"]}, + } + partial = out.with_suffix(".partial") + with zipfile.ZipFile(partial, "w", zipfile.ZIP_DEFLATED) as z: + for name, payload in ((wheel.name, data), + (MANIFEST, (json.dumps(manifest, indent=2, sort_keys=True) + "\n").encode())): + info = zipfile.ZipInfo(name, FIXED_TIME) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o644 << 16 + z.writestr(info, payload) + shutil.move(partial, out) + return out + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--out", required=True, type=Path) + print(build(parser.parse_args().out.resolve())) + + +if __name__ == "__main__": + main() diff --git a/python-multiplatform-ksp/pyproject.toml b/python-multiplatform-ksp/pyproject.toml index 460631868..d5fcbf41c 100644 --- a/python-multiplatform-ksp/pyproject.toml +++ b/python-multiplatform-ksp/pyproject.toml @@ -30,4 +30,7 @@ test = ["pytest"] testpaths = ["src/test/python"] pythonpath = ["src/main/python"] addopts = "--basetemp=.pytest_tmp" -markers = ["measurement: records cost; asserts only sanity bounds"] +markers = [ + "measurement: records cost; asserts only sanity bounds", + "race_mutant: shows a concurrency test catches a mutant; depends on the race firing, so local only (not in CI)", +] diff --git a/python-multiplatform-ksp/src/main/kotlin/python/multiplatform/ksp/BindingPolicy.kt b/python-multiplatform-ksp/src/main/kotlin/python/multiplatform/ksp/BindingPolicy.kt index 04995ed70..c2765b9ac 100644 --- a/python-multiplatform-ksp/src/main/kotlin/python/multiplatform/ksp/BindingPolicy.kt +++ b/python-multiplatform-ksp/src/main/kotlin/python/multiplatform/ksp/BindingPolicy.kt @@ -132,7 +132,7 @@ object BindingPolicy { * Cannot access 'privateSet': it is private in 'fixture.library.RestrictedSetters'. * Cannot access 'topLevelPrivateSet': it is private in file. * - * -- observed on `ksp-fixtures/library`'s `RestrictedSetters` before this check existed. + * -- observed on `python-multiplatform-ksp/fixtures/library`'s `RestrictedSetters` before this check existed. * * `internal set` is excluded for a different reason, and the difference is worth stating * because it does *not* announce itself: `internal` is enforced per Kotlin module and the diff --git a/python-multiplatform-ksp/src/main/python/typedpython/__init__.py b/python-multiplatform-ksp/src/main/python/typedpython/__init__.py index 507809dab..876758d5b 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/__init__.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/__init__.py @@ -1,4 +1,4 @@ -"""TypedPython — static checking for the Python code of a PythonMultiplatform project. +"""TypedPython, static checking for the Python code of a PythonMultiplatform project. See docs/design/typedpython.md. This package is the soundness gate (§4.2). """ diff --git a/python-multiplatform-ksp/src/main/python/typedpython/__main__.py b/python-multiplatform-ksp/src/main/python/typedpython/__main__.py index 6534afebf..bd220e713 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/__main__.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/__main__.py @@ -82,8 +82,12 @@ def _build(paths: list[Path], cache: Path, out: Path | None) -> int: why = f" ({', '.join(m.reasons)})" if m.reasons else "" what = m.extension.name if m.extension else "no extension (interpreted)" print(f"{m.status:7} {m.name}: {what}{why}") - for name, reason in sorted(m.skipped.items()): - print(f" interpreted: {name} — {reason}") + rel = m.path.relative_to(root).as_posix() + if m.kind == "not_marked": + print(f" {rel}: not marked for compilation (no `# typedpython: compiled`, no @compiled)") + for item in m.skipped_items: + where = f"{item.file}:{item.line}" if item.line is not None else item.file + print(f" {where}: {item.name}: {item.message}") if out is not None and m.extension is not None: out.mkdir(parents=True, exist_ok=True) shutil.copy2(m.extension, out / m.extension.name) @@ -131,7 +135,7 @@ def run(load: object) -> tuple[str, float]: compiled_names = [f.name for f in result.module.functions] print(f"compiled functions: {', '.join(compiled_names) or '(none)'}") for name, reason in sorted(result.skipped.items()): - print(f" interpreted: {name} — {reason}") + print(f" interpreted: {name}, {reason}") out_i, t_i = run(interpreted) out_c, t_c = run(lambda: pipeline.load(result)) print(f"interpreted: {t_i * 1000:.1f} ms compiled: {t_c * 1000:.1f} ms " diff --git a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py index f5c4f7b6d..0a9bfe0d2 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py @@ -22,19 +22,66 @@ guard, or the impl's 1 in a pure function) increments `__typedpython_deopts__` and calls `__typedpython_interpreted__[f]` with the original arguments. +Free-threaded CPython 3.15t (issue #159; runtime/API.md "Free-threaded CPython" holds the table of +every piece of shared state). The module slots declare `{Py_mod_gil, Py_MOD_GIL_NOT_USED}` (under +`#ifdef Py_GIL_DISABLED`), so importing it keeps the GIL off. In `tp_wrap_f`, a function with list +parameters holds the lists' critical section (`tp_cs_begin`, one list or two) from before the first +copy-in to after the last write-back: `tp_cs_end` is part of the same `exits` that run the write-back +on every path (ok, error, deopt, the last before the interpreted redo), and is a no-op on a path +that left before locking. More than two list parameters are refused here (`CGenError`), as in the +front end and the verifier: CPython has no three-object critical section. A function that holds an +array does not poll the eval breaker (#141, open decision #152), so a long compiled array loop +delays other threads' stop-the-world requests until it returns: a liveness delay, results +unaffected. The `__typedpython_deopts__` read-add-write of `tp_cg_redo` runs in the module dict's +critical section. Module state is written only by the exec slot; the class guards are atomic +(tp_runtime.h). + +Buffer arrays (issue #149, identity-safe part). A parameter annotated `array.array[float]`, +`array.array[int]` is an `ArrayParam` with `buffer` "array". numpy: not supported yet (#220): +a numpy element's arithmetic produces np.float64 / np.int64 / np.bool (compiled code returns float), +numpy scalar operations emit observable RuntimeWarnings and divide by zero without raising (compiled +double arithmetic does neither), and emulating that needs taint tracking of numpy-derived values, +numpy-scalar boxing and FP-exception-to-warning emulation. An `NDArray[...]` parameter is an ordinary +object parameter. `tp_wrap_f` does not copy it: after the scalar guards, `tp_buf_enter` +checks the exact type (captured in `tp_state.buf_types` by the exec slot), takes one `Py_buffer` +export (C-contiguous, 1-D, 8-byte items, format `d` / `q` / `l`, aligned, writable when the function +stores) and any mismatch deopts before an effect. The export is released by `tp_buf_exit`, part of the +same `exits` as the list write-back, so every path (ok, error, deopt before the interpreted redo) +releases it. Index/StoreIndex are `tp_buf_ld_*` / `tp_buf_st_*`: one relaxed atomic 8-byte access on +the buffer memory (`-DTP_BUFFER_PLAIN` makes them plain, for measuring the atomics only), so there is +no copy-in, no write-back, no critical section, no aliasing guard and no limit on their number; two +buffers that share memory see each other's writes in program order. Closedness (#203) is required as +for lists. OPEN POINT (decision pending in #149): holding the export makes another thread's `append` +/ `resize` of the same array.array raise BufferError during the call; what a concurrent resize +should observe is not decided here and nothing relies on it. A function holding a buffer does not poll +the eval breaker (it counts as a snapshot holder, conservatively). + Depth (issue #57): every impl function calls `tp_enter_call()` before it owns anything (a refusal returns -1 directly: RecursionError set, nothing counted) and `tp_leave_call()` in the one exit block, so ok, error and deopt exits all leave exactly once. +Fast entry (issue #147). A *bounded* function (`_bounded_functions`: no call cycle, every callee +bounded, no node that can run Python code) has a call tree of at most k compiled frames. Its +`tp_impl_f` first tests `tp_depth_room(k)`: with room for all k frames it runs `tp_fimpl_f`, the +twin with the same body and no tp_enter_call / tp_leave_call / entry poll, whose Calls go to the +callees' twins (a `static inline` function in the same unit, so the C compiler can inline it); +without room the counted path runs and raises RecursionError exactly where it always did. Frames +of a fast tree are not in `tp_depth.count`, so every twin takes `int tp_fd`, the number of the +tree's frames active including itself (1 from the precheck, `tp_fd + 1` into a callee twin), and +the two points where a fast tree can run Python code, a `redo` Call and the loop poll's +`tp_poll_slow` (a signal handler, a finalizer, another compiled call), are wrapped in +`tp_depth_py_begin(tp_fd)` / `tp_depth_py_end(tp_fd)`, which add and remove `tp_fd` from the count: +whatever runs there sees the count the counted path would have had. + Ownership rule (ir.py "Safety"), implemented by the `_own_*` emit helpers below and nowhere else: - 1. every OBJ local — including each OBJ parameter, which the impl copies with a new reference at - entry — owns one strong reference, or is NULL; + 1. every OBJ local, including each OBJ parameter, which the impl copies with a new reference at + entry, owns one strong reference, or is NULL; 2. every OBJ-typed expression evaluates into a fresh temporary that owns a new reference; temporaries are NULL at every statement boundary; 3. a temporary is consumed exactly once: moved into a local (the local's old reference is released), moved into `*tp_out` by Return, or released (`tp_release`) right after the helper that borrowed it returns; - 4. every exit of the impl — return, error, deopt — goes through one exit block that releases + 4. every exit of the impl, return, error, deopt, goes through one exit block that releases every OBJ local and temporary. The generated code never decrements a reference except through `tp_release` (API.md); the only increment is `Py_NewRef` when an OBJ local is read or an OBJ parameter is adopted. @@ -44,9 +91,9 @@ (`{0}`), with `list == NULL` and `dirty == NULL`. It is owned by the function: 1. `Assign(local, NewArray | CopyArray)` builds the new array in a zeroed temporary struct (`tp_*_array_new` / `_iota` / `_copy`), and only when that succeeded frees the old array - (`tp_*_array_free`) and moves the temporary into the slot — so a failed allocation leaves the + (`tp_*_array_free`) and moves the temporary into the slot, so a failed allocation leaves the old binding intact, as `a = [0] * n` does, and `a = a[:]` copies before it frees; - 2. every exit of the impl — return, error, deopt — frees every local array and every array + 2. every exit of the impl, return, error, deopt, frees every local array and every array temporary in the same exit block that releases the OBJ locals (a deopt of a pure function frees them before the wrapper redoes the call interpreted); 3. local arrays never go through `tp_*_array_enter/exit` (there is no list to write back to) and @@ -55,6 +102,55 @@ `Tuple` evaluates each element into an owned temporary, builds the tuple with `tp_tuple` (which steals nothing), then releases the temporaries. +Fixed-layout classes (SPEC N-11, ir.ClassDecl). The module state holds, per ClassDecl, a strong +reference to the class object, a `cls_ok` flag and one captured `PyMemberDef *` per field. The exec +slot fills them AFTER the original source ran (`tp_class_capture`: a heap type made by `type`, base +object, no __dict__, every field a writable object slot made for that very class); a class that fails +any check is "not compiled" (`cls_ok = 0`) and is never touched through memory: + 1. every wrapper first tests `cls_ok` of every class its function uses *or reaches through C to C + Calls* (`_ModGen.uses`), else it takes the interpreted path (a static property of the module, so + callees need no check of their own); + 2. a `Param.cls` guard (`type(x) is C`, or `x is None` when optional) runs in the wrapper AND at the + top of the impl, before tp_enter_call and before anything is owned: the wrapper's copy serves + impure functions (whose impl may never return 1), the impl's copy serves C to C Calls, where a + failure is a deopt, so a Call of a function with a `Param.cls` parameter counts as a node that + can deopt (`_can_deopt`), exactly like a callee with entry_globals; + 3. FieldGet / FieldSet / New go through `tp_field_get` / `tp_field_set` / `tp_new_fixed` with the + captured descriptors; CheckExact deopts (return 1) on mismatch and is legal only in a pure + function; Is is a plain BOOL. + +Classes changed after init (the class, the module global or an instance's __class__). The entry +guards above only select the path; correctness never depends on them, because user code can change +a class between calls or, in an impure function, between entry and the access. Every access +re-proves what it relies on, right where CPython would look: + 4. FieldGet / FieldSet: `tp_class_current(obj, ...)`, `type(obj) is C` and C's tp_version_tag is + the tag captured at init (or re-proved by `tp_class_refresh`, which adopts a new tag when every + field is still the very member descriptor and __init__ the very function captured at init: + `Node.counter = 1` costs one refresh, not a slow path forever). One type compare and one tag + compare when nothing changed. + 5. New: `tp_class_global_ok` BEFORE the arguments are evaluated (CPython's LOAD_GLOBAL comes first: + `Node(cb(), b)` builds the class bound when Node was loaded), then `tp_class_current(NULL, ...)` + after them (type.__call__ sees __init__ as it is at the call). The global test is one compare + while a dict watcher on the module dict (`tp_globals_watch`, its id in the module state) has not + reported a change of a class-name key (`tp_globals_epoch`); without a free watcher id it is a + dict lookup per New (`watch_id1 == 0`, the conservative path). + 6. IsExact: `type(obj) is C` with C looked up as a global (`tp_class_global_ok`, then the current + binding on the slow path). + When a check fails: in an impure function, the slow path with CPython's semantics and no deopt + (FieldGet -> tp_getattr, FieldSet -> tp_setattr, New -> call what the global held when it was + loaded, IsExact -> compare with the current global); in a pure function, a deopt (return 1), + running the property or __init__ there could repeat its effect if a later node deopted and the + call were redone. A pure function may thus return 1 even when `may_deopt` is False, so: + 7. an impure caller of a pure callee that uses classes, in a Call without `redo`, redoes that + callee interpreted on a 1 (`_redo_call`, any return type), the callee deopted before running + any user code, so the redo is unobservable; the impure caller itself never deopts; + 8. an impure function with entry globals (closed: it runs no user code, ir.Function.entry_globals) + checks at entry, in the wrapper, that every class it reaches is current and every class global + it reads is still bound: then no check inside can fail, so no slow path can run user code that + rebinds an entry global after its snapshot. A failure there is a deopt before any effect. + `__typedpython_class_info__()` reports, per class, the captured and current tags, the slow-path + and refresh counters and whether the module dict is watched (introspection for tests). + Generated arithmetic is in separate C statements and the file sets `#pragma STDC FP_CONTRACT OFF` (cbuild also passes -ffp-contract=off): a fused multiply-add would round differently from CPython. """ @@ -72,6 +168,11 @@ class CGenError(Exception): _CT = {Type.I64: "int64_t", Type.F64: "double", Type.BOOL: "int", Type.OBJ: "PyObject *"} _ARRAY_CT = {Type.F64_ARRAY: "tp_f64_array", Type.I64_ARRAY: "tp_i64_array"} +# Buffer arrays (#149): `ArrayParam.buffer` -> the exact type the guard compares against (the index +# into the module state's `buf_types`), and the format strings the export must carry. +_BUFFER_KINDS = {None: None, "array": 0} +_BUFFER_FMT = {Type.F64_ARRAY: ('"d"', "NULL"), Type.I64_ARRAY: ('"q"', '"l"')} +_BUFFER_SFX = {Type.F64_ARRAY: "f64", Type.I64_ARRAY: "i64"} _CT_ARRAY_REV = {"tp_f64_array": Type.F64_ARRAY, "tp_i64_array": Type.I64_ARRAY} _ARRAY_ELEM = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64} _ARRAY_PFX = {Type.F64_ARRAY: "tp_f64_array", Type.I64_ARRAY: "tp_i64_array"} @@ -93,6 +194,18 @@ class CGenError(Exception): _I64_DEOPT = {ir.BinOpKind.ADD, ir.BinOpKind.SUB, ir.BinOpKind.MUL, ir.BinOpKind.FLOORDIV, ir.BinOpKind.TRUEDIV} _INT64_MIN, _INT64_MAX = -(2 ** 63), 2 ** 63 - 1 +# `proven` on an I64 op (ir.BinOp.proven): ADD/SUB/MUL cannot overflow; FLOORDIV/MOD by a Const +# power of two of a value >= 0 become a shift / mask. +_SHIFT_OPS = {ir.BinOpKind.FLOORDIV, ir.BinOpKind.MOD} +_PROVEN_OPS = {ir.BinOpKind.ADD, ir.BinOpKind.SUB, ir.BinOpKind.MUL} | _SHIFT_OPS + + +def _pow2_shift(divisor: ir.Expr) -> int | None: + """k when `divisor` is the Const 2**k (k >= 0), else None.""" + v = divisor.value if isinstance(divisor, ir.Const) and divisor.type is Type.I64 else None + if type(v) is int and v >= 1 and v & (v - 1) == 0: + return v.bit_length() - 1 + return None # --- C literals and identifiers ------------------------------------------------------------------ @@ -156,6 +269,15 @@ def _exprs_of_stmt(s: ir.Stmt): elif isinstance(s, ir.StoreIndex): yield s.value yield s.index + elif isinstance(s, ir.FieldSet): + yield s.value + yield s.obj + elif isinstance(s, ir.StoreItem): + yield s.value + yield s.obj + yield s.index + elif isinstance(s, ir.ForObj): + yield s.iter elif isinstance(s, ir.ExprStmt): yield s.value elif isinstance(s, ir.If): @@ -172,7 +294,7 @@ def _exprs_of_stmt(s: ir.Stmt): def _children(s: ir.Stmt): if isinstance(s, ir.If): return s.then + s.orelse - if isinstance(s, (ir.While, ir.ForRange)): + if isinstance(s, (ir.While, ir.ForRange, ir.ForObj)): return s.body return () @@ -208,26 +330,128 @@ def _all_exprs(f: ir.Function): def _can_deopt(e: ir.Expr, functions: dict[str, ir.Function]) -> bool: if isinstance(e, ir.BinOp) and e.left.type is Type.I64 and e.op in _I64_DEOPT: - return not e.proven # a proven op cannot deopt (overflow = SystemError) + return not e.proven # a proven op cannot deopt (no overflow, or a shift / mask) if isinstance(e, ir.UnaryOp) and e.op is ir.UnaryOpKind.NEG and e.operand.type is Type.I64: return not e.proven - if isinstance(e, ir.Unbox): + if isinstance(e, (ir.Unbox, ir.CheckExact)): return True if isinstance(e, ir.Call): if e.redo: return False # the callee's deopt is absorbed by its interpreted redo callee = functions.get(e.function) # a callee's entry-global guard runs at the call (`_FnGen.ev_Call`) and can fail there - return callee is not None and (callee.may_deopt or bool(callee.entry_globals)) + return callee is not None and (callee.may_deopt or bool(callee.entry_globals) + or _has_cls_param(callee)) return False +def _has_cls_param(f: ir.Function) -> bool: + """A Param.cls guard runs at every call of `f`, C to C included, and a failure is a deopt.""" + return any(isinstance(p, ir.Param) and p.cls is not None for p in f.params) + + +def _direct_classes(f: ir.Function) -> set[str]: + out = {p.cls for p in f.params if isinstance(p, ir.Param) and p.cls is not None} + for e in _all_exprs(f): + if isinstance(e, (ir.IsExact, ir.CheckExact, ir.FieldGet, ir.New)): + out.add(e.cls) + for s in _walk_stmts(f.body): + if isinstance(s, ir.FieldSet): + out.add(s.cls) + return out + + +def _param_cls_fail(obj_c: str, k: int, optional: bool) -> str: + """The C condition under which a `Param.cls` guard FAILS for the borrowed argument `obj_c` + (class slot `k` of the module state): not exactly the class, and not None when optional. + Emitted both in the wrapper and at the top of the impl (module docstring).""" + exact = f"tp_is_exact({obj_c}, (PyTypeObject *)tp_st->classes[{k}])" + return f"!({exact} || {obj_c} == Py_None)" if optional else f"!{exact}" + + +def _class_current_expr(obj_c: str, k: int, fields_c: str, n: int, descrs_c: str) -> str: + """The per-access check of FieldGet / FieldSet (`obj_c` a C object) and New (`obj_c` "NULL"): + 1 current, 0 slow path, -1 error (module docstring, rules 4 and 5).""" + return (f"tp_class_current({obj_c}, tp_st->classes[{k}], &tp_st->cls_rt[{k}], " + f"{fields_c}, {n}, {descrs_c})") + + +def _watch_event_stmt() -> str: + """The body of the module's dict-watcher callback (module docstring, rule 5).""" + return "return tp_globals_watch_event(event, key, tp_class_names);" + + def _is_effect(e: ir.Expr) -> bool: - if isinstance(e, (ir.GetAttr, ir.CallObject, ir.Truth, ir.CompareObj, ir.ObjToFloat)): + if isinstance(e, (ir.GetAttr, ir.CallObject, ir.Truth, ir.CompareObj, ir.ObjToFloat, + ir.GetItem, ir.Contains, ir.Format)): return True return isinstance(e, ir.BinOp) and e.type is Type.OBJ + +# --- bounded functions (fast entry, issue #147) ---------------------------------------------------- + +# Nodes that can run Python code (and so create Python frames the compiled frame count must see): +# their slow paths call getattr, the class, __bool__, __float__, a Python callable, a descriptor. +_RUNS_PYTHON = (ir.CallObject, ir.GetAttr, ir.Truth, ir.CompareObj, ir.ObjToFloat, ir.FieldGet, + ir.FieldSet, ir.New, ir.GetItem, ir.Contains, ir.Format, ir.StoreItem, ir.ForObj) + + +def _runs_no_python(f: ir.Function) -> bool: + """Conservative: False when `f` has any node that can run Python code, or touches an object + other than a fresh exact int, float or bool made by Box of a scalar (an OBJ parameter, local, + result or any other OBJ expression: releasing one can run a finalizer). A `redo` Call is + allowed (its callee is bounded, checked by the caller): the redo is one of the two points where + a fast tree runs Python code, and `_redo_call` counts the tree's frames around it.""" + if f.returns is Type.OBJ or f.entry_globals and any(g.type is Type.OBJ for g in f.entry_globals): + return False + if any(isinstance(p, ir.Param) and p.type is Type.OBJ for p in f.params): + return False + if any(t is Type.OBJ for t in f.locals.values()): + return False + for s in _walk_stmts(f.body): + if isinstance(s, (ir.FieldSet, ir.StoreItem, ir.ForObj)): + return False + for e in _all_exprs(f): + if isinstance(e, _RUNS_PYTHON): + return False + if e.type is Type.OBJ and not (isinstance(e, ir.Box) and e.operand.type in ir.SCALARS): + return False + return True + + +def _bounded_functions(functions: dict[str, ir.Function]) -> dict[str, int]: + """name -> k for every bounded function (module docstring, "Fast entry"): in no call cycle, + every callee bounded, `_runs_no_python`. k(f) = 1 + max(k(callee)), 1 without callees: the most + compiled frames a call of `f` can have in flight at once.""" + callees: dict[str, set[str]] = {} + for name, f in functions.items(): + callees[name] = {e.function for e in _all_exprs(f) if isinstance(e, ir.Call)} + k: dict[str, int | None] = {} # None: not bounded (also while on the DFS stack) + + def visit(name: str) -> int | None: + if name in k: + return k[name] + k[name] = None # on the stack: reaching it again is a cycle + f = functions[name] + if not _runs_no_python(f): + return None + deepest = 0 + for c in callees[name]: + if c not in functions: + return None + kc = visit(c) + if kc is None: + return None + deepest = max(deepest, kc) + k[name] = deepest + 1 + return k[name] + + for name in functions: + visit(name) + return {n: v for n, v in k.items() if v is not None} + + # --- module -------------------------------------------------------------------------------------- class _ModGen: @@ -238,6 +462,76 @@ def __init__(self, module: ir.Module): raise CGenError("duplicate compiled function names") self.names: list[str] = [] self.kwtuples: list[tuple[str, ...]] = [] + self.classes = {cd.name: cd for cd in module.classes} + if len(self.classes) != len(module.classes): + raise CGenError("duplicate ClassDecl names") + self.class_index = {cd.name: i for i, cd in enumerate(module.classes)} + self.slot_base: dict[str, int] = {} + total = 0 + for cd in module.classes: + if len(set(cd.fields)) != len(cd.fields): + raise CGenError(f"class {cd.name}: duplicate field names") + self.slot_base[cd.name] = total + total += len(cd.fields) + self.n_slots = total + self._uses: dict[str, set[str]] = {} + self.bounded = _bounded_functions(self.functions) # name -> k (module docstring, "Fast entry") + + def cls(self, fname: str, name: str) -> ir.ClassDecl: + cd = self.classes.get(name) + if cd is None: + raise CGenError(f"{fname}: class {name!r} has no ClassDecl in the module") + return cd + + def slot(self, fname: str, cls: str, field: str) -> str: + cd = self.cls(fname, cls) + if field not in cd.fields: + raise CGenError(f"{fname}: class {cls} has no field {field!r}") + return f"tp_st->slots[{self.slot_base[cls] + cd.fields.index(field)}]" + + def uses(self, f: ir.Function) -> list[str]: + """Every class `f` uses or reaches through C to C Calls (module docstring, rule 1).""" + if not self._uses: + for g in self.module.functions: + self._uses[g.name] = _direct_classes(g) + changed = True + while changed: + changed = False + for g in self.module.functions: + for e in _all_exprs(g): + if isinstance(e, ir.Call) and e.function in self._uses: + extra = self._uses[e.function] - self._uses[g.name] + if extra: + self._uses[g.name] |= extra + changed = True + return sorted(self._uses[f.name], key=lambda n: self.class_index.get(n, -1)) + + def fields_c(self, cls: str) -> tuple[str, int, str]: + """(C fields array, field count, C descriptor-array pointer) of class `cls`.""" + cd = self.classes[cls] + k, base = self.class_index[cls], self.slot_base[cls] + return f"tp_fields_{k}", len(cd.fields), f"&tp_st->descrs[{base}]" + + def globals_read(self, f: ir.Function) -> list[str]: + """Classes whose module global `f` reads (New, IsExact) itself or through C to C Calls.""" + out: set[str] = set() + seen: set[str] = set() + todo = [f.name] + while todo: + g = self.functions[todo.pop()] + if g.name in seen: + continue + seen.add(g.name) + for e in _all_exprs(g): + if isinstance(e, (ir.New, ir.IsExact)): + out.add(e.cls) + if isinstance(e, ir.Call) and e.function in self.functions: + todo.append(e.function) + return sorted(out, key=lambda n: self.class_index.get(n, -1)) + + def watches_globals(self) -> bool: + return any(isinstance(e, (ir.New, ir.IsExact)) + for f in self.module.functions for e in _all_exprs(f)) def name_index(self, s: str) -> int: if s not in self.names: @@ -252,9 +546,16 @@ def kw_index(self, kw: tuple[str, ...]) -> int: return self.kwtuples.index(kw) -def generate(module: ir.Module, source_path: Path) -> str: - """One C file implementing `module`; `source_path` is the original Python source, embedded.""" +def generate(module: ir.Module, source_path: Path, display_path: str | None = None) -> str: + """One C file implementing `module`; `source_path` is the original Python source, embedded. + + `display_path` is the path embedded as the code object's filename (tracebacks of the interpreted + fallback): the module's path relative to the project root, posix separators (#112). It never + defaults to the absolute host path, so the C does not depend on where the project was built; + left out, it is the file name. + """ source_path = Path(source_path) + display_path = source_path.name if display_path is None else display_path source = source_path.read_text(encoding="utf-8") mg = _ModGen(module) for f in module.functions: @@ -268,7 +569,7 @@ def generate(module: ir.Module, source_path: Path) -> str: out: list[str] = [] w = out.append - w(f"/* Generated by typedpython.cgen from {source_path.name} — do not edit. */") + w(f"/* Generated by typedpython.cgen from {source_path.name}, do not edit. */") w("#define PY_SSIZE_T_CLEAN") w("#include ") w("#include ") @@ -280,7 +581,7 @@ def generate(module: ir.Module, source_path: Path) -> str: w("#pragma STDC FP_CONTRACT OFF") w("") w(f"static const char tp_source[] =\n {c_string(source)};") - w(f"static const char tp_source_path[] = {c_string(str(source_path))};") + w(f"static const char tp_source_path[] = {c_string(display_path)};") w("") n_names, n_kw = len(mg.names), len(mg.kwtuples) w("/* Module state: interned names (globals, attributes, keywords, function names) and the") @@ -288,6 +589,14 @@ def generate(module: ir.Module, source_path: Path) -> str: w("typedef struct {") w(f" PyObject *names[{n_names + 1}];") w(f" PyObject *kwnames[{n_kw + 1}];") + w(f" PyObject *classes[{len(module.classes) + 1}]; /* strong refs; NULL when not compiled */") + w(f" int cls_ok[{len(module.classes) + 1}];") + w(f" PyMemberDef *slots[{mg.n_slots + 1}]; /* owned by the classes above */") + w(f" PyObject *descrs[{mg.n_slots + 1}]; /* strong: the member descriptor of each slot */") + w(f" tp_class_rt cls_rt[{len(module.classes) + 1}]; /* version-tag / global guard state */") + w(" int watch_id1; /* dict watcher id + 1 on the module dict; 0 = none */") + w(" PyObject *breaker; /* `lambda: None`; calling it runs the eval breaker (#141) */") + w(" PyObject *buf_types[1]; /* strong: array.array (buffer arrays, #149); NULL: not importable */") w("} tp_state;") w("") w("static const char *const tp_name_strings[] = {") @@ -296,8 +605,24 @@ def generate(module: ir.Module, source_path: Path) -> str: w(" NULL") w("};") w("") + for cd in module.classes: + items = "".join(f"{c_string(x)}, " for x in cd.fields) + w(f"static const char *const tp_fields_{mg.class_index[cd.name]}[] = {{{items}NULL}};") + names = "".join(f"{c_string(cd.name)}, " for cd in module.classes) + w(f"static const char *const tp_class_names[] = {{{names}NULL}};") + w("") + if module.classes and mg.watches_globals(): + w("/* Dict watcher of the module dict (tp_globals_watch): a class-name key changed. */") + w("static int tp_watch_cb(PyDict_WatchEvent event, PyObject *dict, PyObject *key, PyObject *value)") + w("{") + w(" (void)dict; (void)value;") + w(f" {_watch_event_stmt()}") + w("}") + w("") for f in module.functions: w(_impl_signature(f) + ";") + if f.name in mg.bounded: + w(_impl_signature(f, fast=True) + ";") w("") w(_TEMPLATE_REDO) for b in bodies: @@ -307,13 +632,21 @@ def generate(module: ir.Module, source_path: Path) -> str: for f in module.functions: w(f" {{{c_string(f.name)}, (PyCFunction)(void (*)(void)){_ident('tp_wrap', f.name)}, " f"METH_FASTCALL | METH_KEYWORDS, NULL}},") + w("#ifdef TP_TRACE_FAST") + w(' {"__tp_trace_fast_hits__", (PyCFunction)tp_trace_fast_hits, METH_NOARGS, NULL},') + w("#endif") w(" {NULL, NULL, 0, NULL}") w("};") w("") + if module.classes: + w(_class_info_function(mg)) w(_exec_function(mg)) - w(_TEMPLATE_STATE) + w(_state_functions(mg)) w("static PyModuleDef_Slot tp_slots[] = {") w(" {Py_mod_exec, (void *)tp_exec},") + w("#ifdef Py_GIL_DISABLED") + w(" {Py_mod_gil, Py_MOD_GIL_NOT_USED}, /* free-threaded safe: runtime/API.md \"Free-threaded CPython\" */") + w("#endif") w(" {0, NULL}") w("};") w("") @@ -337,18 +670,23 @@ def _check_function(f: ir.Function, functions: dict[str, ir.Function]) -> None: effects = [e for e in _all_exprs(f) if _is_effect(e)] # a store into a local array is not an effect (ir.py "Local arrays"); into a parameter it is stores = [s for s in _walk_stmts(f.body) - if isinstance(s, ir.StoreIndex) and f.locals.get(s.array) not in ir.ARRAYS] + if (isinstance(s, ir.StoreIndex) and f.locals.get(s.array) not in ir.ARRAYS) + or isinstance(s, (ir.FieldSet, ir.StoreItem, ir.ForObj))] if f.pure and (effects or stores): - what = type(effects[0]).__name__ if effects else "StoreIndex" + what = type(effects[0]).__name__ if effects else type(stores[0]).__name__ raise CGenError(f"{f.name}: marked pure but contains an effect ({what})") for e in _all_exprs(f): if isinstance(e, (ir.BinOp, ir.UnaryOp)) and e.proven: - checked = (e.op in (ir.BinOpKind.ADD, ir.BinOpKind.SUB, ir.BinOpKind.MUL) - if isinstance(e, ir.BinOp) else e.op is ir.UnaryOpKind.NEG) + checked = (e.op in _PROVEN_OPS if isinstance(e, ir.BinOp) + else e.op is ir.UnaryOpKind.NEG) operand = e.left if isinstance(e, ir.BinOp) else e.operand if not checked or operand.type is not Type.I64: raise CGenError(f"{f.name}: proven is set on {e.op.name} of {operand.type} " - "(ir.py: I64 ADD/SUB/MUL and NEG only)") + "(ir.py: I64 ADD/SUB/MUL, FLOORDIV/MOD by a power of two, and NEG " + "only)") + if isinstance(e, ir.BinOp) and e.op in _SHIFT_OPS and _pow2_shift(e.right) is None: + raise CGenError(f"{f.name}: proven {e.op.name} needs a Const power-of-two divisor " + f"(ir.py), got {e.right!r}") if isinstance(e, ir.Call): callee = functions.get(e.function) if callee is None: @@ -362,28 +700,40 @@ def _check_function(f: ir.Function, functions: dict[str, ir.Function]) -> None: if not callee.pure: raise CGenError(f"{f.name}: redo Call of impure {e.function!r} " "(redoing it could repeat an effect)") - elif (callee.may_deopt or callee.entry_globals) and not f.pure: + elif (callee.may_deopt or callee.entry_globals or _has_cls_param(callee)) and not f.pure: raise CGenError(f"{f.name}: Call of deopting {e.function!r} from an impure caller " "without redo") if f.entry_globals: _check_entry_globals(f, functions) + n_arrays = sum(isinstance(p, ir.ArrayParam) and p.buffer is None for p in f.params) + if n_arrays > 2: + raise CGenError(f"{f.name}: {n_arrays} array parameters; at most two (CPython offers one- and " + "two-object critical sections only, and a third lock could suspend the others)") for p in f.params: if isinstance(p, ir.ArrayParam): if p.type not in ir.ARRAYS: raise CGenError(f"{f.name}: array param {p.name} has type {p.type}") + if p.buffer not in _BUFFER_KINDS: + raise CGenError(f"{f.name}: array param {p.name} has buffer kind {p.buffer!r}") if p.stored and f.pure: raise CGenError(f"{f.name}: array param {p.name} is stored into in a pure function") elif p.type not in _CT: raise CGenError(f"{f.name}: param {p.name} has type {p.type}") + elif p.cls is not None and p.type is not Type.OBJ: + raise CGenError(f"{f.name}: param {p.name} has a class guard but type {p.type}") + elif p.optional and p.cls is None: + raise CGenError(f"{f.name}: param {p.name} is optional without a class") def _is_closed(f: ir.Function, functions: dict[str, ir.Function], seen=None) -> bool: - """ir.Function.entry_globals: runs no user code — no effect node, Calls only to closed - functions — so nothing can rebind a module global while it runs.""" + """ir.Function.entry_globals: runs no user code, no effect node, Calls only to closed + functions, so nothing can rebind a module global while it runs.""" seen = set() if seen is None else seen if f.name in seen: return True seen.add(f.name) + if any(isinstance(s, (ir.StoreItem, ir.ForObj)) for s in _walk_stmts(f.body)): + return False for e in _all_exprs(f): if _is_effect(e): return False @@ -414,11 +764,13 @@ def _check_entry_globals(f: ir.Function, functions: dict[str, ir.Function]) -> N "user code, which could rebind the global between entry and use)") -def _impl_signature(f: ir.Function) -> str: +def _impl_signature(f: ir.Function, fast: bool = False) -> str: params = ["PyObject *tp_module"] + if fast: + params.append("int tp_fd") # compiled frames of this fast tree, itself included for p in f.params: if isinstance(p, ir.ArrayParam): - params.append(f"{_ARRAY_CT[p.type]} *{_ident('l', p.name)}") + params.append(f"{'tp_buf' if p.buffer else _ARRAY_CT[p.type]} *{_ident('l', p.name)}") elif p.type is Type.OBJ: params.append(f"PyObject *{_ident('p', p.name)}") else: @@ -428,14 +780,29 @@ def _impl_signature(f: ir.Function) -> str: if f.returns is not Type.NONE: ct = _CT[f.returns] params.append(f"{ct}{'' if ct.endswith('*') else ' '}*tp_out") + if fast: + return f"static inline int {_ident('tp_fimpl', f.name)}({', '.join(params)})" return f"static int {_ident('tp_impl', f.name)}({', '.join(params)})" +def _impl_call_args(f: ir.Function, fd: str | None = None) -> str: + """The arguments that forward an impl's own parameters to another impl of the same signature + (`fd`: the value of a fast twin's `tp_fd`).""" + args = ["tp_module"] + ([fd] if fd is not None else []) + for p in f.params: + args.append(_ident("l" if isinstance(p, ir.ArrayParam) or p.type is not Type.OBJ else "p", p.name)) + args += [_ident("l", g.name) for g in f.entry_globals] + if f.returns is not Type.NONE: + args.append("tp_out") + return ", ".join(args) + + # --- function bodies ----------------------------------------------------------------------------- class _FnGen: - def __init__(self, mg: _ModGen, f: ir.Function): + def __init__(self, mg: _ModGen, f: ir.Function, fast: bool = False): self.mg, self.f = mg, f + self.fast = fast # the uncounted twin of a bounded function self.lines: list[str] = [] self.depth = 1 self.temps: list[tuple[str, str]] = [] # (C type, name) @@ -474,18 +841,10 @@ def check(self, call: str) -> None: self.emit(f"tp_s = {call};") self.emit("if (tp_s != 0) { tp_rc = tp_s; goto tp_exit; }") - def check_proven(self, call: str) -> None: - """A checked I64 helper on a `proven` op (ir.BinOp.proven): it can only return 0 or 1, and - 1 means the front end's and the verifier's proof was wrong — an internal error, never a - deopt (a proven op may sit after an effect).""" - self.emit(f"tp_s = {call};") - self.emit("if (tp_s != 0) { PyErr_SetString(PyExc_SystemError, " - "\"typedpython: proven operation overflowed\"); tp_rc = -1; goto tp_exit; }") - def fail(self) -> None: self.emit("{ tp_rc = -1; goto tp_exit; }") - # ownership helpers — the only places that create or drop references (see module docstring) + # ownership helpers, the only places that create or drop references (see module docstring) def _own_new(self, call: str) -> str: """A fresh temporary owning the new reference `call` returns (NULL -> error exit).""" @@ -543,6 +902,9 @@ def ev_Const(self, e: ir.Const) -> str: return self._own_copy("Py_None") if isinstance(e.value, bool): return self._own_copy("Py_True" if e.value else "Py_False") + if type(e.value) is str and "\0" not in e.value: + # a str constant: the interned object the module state holds (names table) + return self._own_copy(f"tp_st->names[{self.mg.name_index(e.value)}]") raise CGenError(f"{self.f.name}: OBJ constant {e.value!r} (use Box of a scalar Const)") raise CGenError(f"{self.f.name}: constant of type {e.type}") @@ -563,8 +925,15 @@ def ev_BinOp(self, e: ir.BinOp) -> str: a, b = self.ev(e.left), self.ev(e.right) if lt is Type.I64: t = self.tmp("double" if e.op is ir.BinOpKind.TRUEDIV else "int64_t") - if e.proven: - self.check_proven(f"{_I64_OP[e.op]}({a}, {b}, &{t})") + if e.proven and e.op in _SHIFT_OPS: + # a >= 0 and the divisor is 2**k (verifier): Python's // and % equal >> and & + k = _pow2_shift(e.right) + if e.op is ir.BinOpKind.FLOORDIV: + self.emit(f"{t} = {a} >> {k};") + else: + self.emit(f"{t} = {a} & {_i64_lit((1 << k) - 1)};") + elif e.proven: + self.emit(f"tp_proven({e.op.name.lower()}, {a}, {b}, {t});") else: self.check(f"{_I64_OP[e.op]}({a}, {b}, &{t})") return t @@ -577,7 +946,8 @@ def ev_BinOp(self, e: ir.BinOp) -> str: return t if lt is Type.OBJ: t = self.tmp("PyObject *") - self.emit(f"{t} = tp_binop_obj({a}, {b}, {_OBJ_OP[e.op]});") + op = _OBJ_OP[e.op] if not e.inplace else f"TP_BINOP_I{e.op.name}" + self.emit(f"{t} = tp_binop_obj({a}, {b}, {op});") self._own_release(a, b) self.emit(f"if ({t} == NULL) {{ tp_rc = -1; goto tp_exit; }}") return t @@ -595,7 +965,7 @@ def ev_UnaryOp(self, e: ir.UnaryOp) -> str: if ot is Type.I64 and e.op is ir.UnaryOpKind.NEG: t = self.tmp("int64_t") if e.proven: - self.check_proven(f"tp_neg_i64({v}, &{t})") + self.emit(f"tp_proven_neg({v}, {t});") else: self.check(f"tp_neg_i64({v}, &{t})") return t @@ -733,11 +1103,19 @@ def ev_Call(self, e: ir.Call) -> str | None: self.emit("if (tp_s == 0) {") self.depth += 1 # Depth is counted by the callee itself (tp_enter_call at its entry, issue #57). - self.emit(f"tp_s = {_ident('tp_impl', e.function)}(tp_module{''.join(', ' + x for x in args)});") + if self.fast: + # the fast twin's callees are bounded too (_bounded_functions): their fast twins, uncounted + self.emit(f"tp_s = {_ident('tp_fimpl', e.function)}(tp_module, tp_fd + 1{''.join(', ' + x for x in args)});") + else: + self.emit(f"tp_s = {_ident('tp_impl', e.function)}(tp_module{''.join(', ' + x for x in args)});") self.depth -= 1 self.emit("}") if e.redo: self._redo_call(callee, e.args, values, t) + elif not self.f.pure and callee.pure and self.mg.uses(callee): + # a pure callee using classes returns 1 when a class check fails (module docstring, + # rule 7): redo it interpreted; this impure caller must not deopt + self._redo_call(callee, e.args, values, t) self._own_release(*owned) self.emit("if (tp_s != 0) { tp_rc = tp_s; goto tp_exit; }") return t @@ -781,26 +1159,166 @@ def _redo_call(self, callee: ir.Function, arg_exprs, values: list[str], t: str | boxes.append(b) av.append(b) rr = self.tmp("PyObject *") + # In a fast twin the interpreted callee (a Python frame) and whatever it calls must see the + # frames of this fast tree: count them for exactly the span that can run Python code, from + # the call to the release of its result (module docstring, "Fast entry"). + end = "tp_depth_py_end(tp_fd); " if self.fast else "" + if self.fast: + self.emit("tp_depth_py_begin(tp_fd);") if av: self.emit(f"{{ PyObject *tp_av[{len(av)}] = {{{', '.join(av)}}};") self.emit(f" {rr} = tp_cg_redo(tp_module, {k}, tp_av, {len(av)}, NULL); }}") else: self.emit(f"{rr} = tp_cg_redo(tp_module, {k}, NULL, 0, NULL);") self._own_release(*boxes) - self.emit(f"if ({rr} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + self.emit(f"if ({rr} == NULL) {{ {end}tp_rc = -1; goto tp_exit; }}") rt = callee.returns if rt is Type.NONE: self.emit(f"tp_s = ({rr} == Py_None) ? 0 : 1;") what = "None" + elif rt is Type.OBJ: + self.emit(f"{t} = {rr}; {rr} = NULL; tp_s = 0;") # moved: the temp owns it now + what = "an object" else: self.emit(f"tp_s = {_UNBOX[rt]}({rr}, &{t});") - what = {Type.F64: "a float", Type.BOOL: "True or False"}[rt] + what = {Type.F64: "a float", Type.BOOL: "True or False", Type.I64: "an int in i64"}[rt] self._own_release(rr) + if self.fast: + self.emit("tp_depth_py_end(tp_fd);") msg = c_string(f"typedpython: the interpreted redo of {callee.name} did not return {what}") self.emit(f"if (tp_s != 0) {{ PyErr_SetString(PyExc_SystemError, {msg}); tp_rc = -1; goto tp_exit; }}") self.depth -= 1 self.emit("}") + def _obj_operand(self, e: ir.Expr, what: str) -> str: + if e.type is not Type.OBJ: + raise CGenError(f"{self.f.name}: {what} on {e.type}") + return self.ev(e) + + def ev_Is(self, e: ir.Is) -> str: + a = self._obj_operand(e.left, "Is") + b = self._obj_operand(e.right, "Is") + t = self.tmp("int") + self.emit(f"{t} = ({a} {'!=' if e.negate else '=='} {b});") + self._own_release(a, b) + return t + + def _current(self, obj_c: str, cls: str) -> str: + k = self._class(cls) + fields_c, n, descrs_c = self.mg.fields_c(cls) + return _class_current_expr(obj_c, k, fields_c, n, descrs_c) + + def _global_ok(self, cls: str) -> str: + k = self._class(cls) + nk = self.mg.name_index(cls) + return (f"tp_class_global_ok(tp_dict, tp_st->names[{nk}], tp_st->classes[{k}], " + f"&tp_st->cls_rt[{k}], tp_st->watch_id1 > 0)") + + def _slow(self, cls: str) -> None: + """Count a failed per-access check; in a pure function it is a deopt (module docstring).""" + self.emit(f"tp_class_note_slow(&tp_st->cls_rt[{self._class(cls)}]);") + if self.f.pure: + self.emit("tp_rc = 1; goto tp_exit;") + + def ev_IsExact(self, e: ir.IsExact) -> str: + k = self._class(e.cls) + o = self._obj_operand(e.obj, "IsExact") + t = self.tmp("int") + self.emit(f"tp_s = {self._global_ok(e.cls)};") + self.emit("if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + self.emit("if (tp_s) {") + self.emit(f" {t} = tp_is_exact({o}, (PyTypeObject *)tp_st->classes[{k}]);") + self.emit("} else {") + self.depth += 1 + self._slow(e.cls) + if not self.f.pure: + g = self._own_new(f"tp_global(tp_dict, tp_st->names[{self.mg.name_index(e.cls)}])") + self.emit(f"{t} = tp_is_exact({o}, (PyTypeObject *){g});") + self._own_release(g) + self.depth -= 1 + self.emit("}") + self._own_release(o) + return t + + def ev_CheckExact(self, e: ir.CheckExact) -> str: + k = self._class(e.cls) + o = self._obj_operand(e.obj, "CheckExact") + # a deopt: legal only in a pure function (_check_function); the exit block releases `o` + self.emit(f"if (!tp_is_exact({o}, (PyTypeObject *)tp_st->classes[{k}])) {{ tp_rc = 1; goto tp_exit; }}") + return o + + def ev_FieldGet(self, e: ir.FieldGet) -> str: + slot = self.mg.slot(self.f.name, e.cls, e.field) + o = self._obj_operand(e.obj, "FieldGet") + t = self.tmp("PyObject *") + self.emit(f"tp_s = {self._current(o, e.cls)};") + self.emit("if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + self.emit("if (tp_s) {") + self.emit(f" {t} = tp_field_get({o}, {slot});") + self.emit("} else {") + self.depth += 1 + self._slow(e.cls) + if not self.f.pure: # LOAD_ATTR, whatever the class is now + self.emit(f"{t} = tp_getattr({o}, tp_st->names[{self.mg.name_index(e.field)}]);") + self.depth -= 1 + self.emit("}") + self._own_release(o) + self.emit(f"if ({t} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + return t + + def ev_New(self, e: ir.New) -> str: + cd = self.mg.cls(self.f.name, e.cls) + if not cd.trivial_init: + raise CGenError(f"{self.f.name}: New of class {e.cls}, whose __init__ is not trivial") + if len(e.args) != len(cd.fields): + raise CGenError(f"{self.f.name}: New {e.cls} with {len(e.args)} args for " + f"{len(cd.fields)} fields") + k = self._class(e.cls) + # LOAD_GLOBAL C comes before the arguments (module docstring, rule 5) + gk, g = self.tmp("int"), None + self.emit(f"tp_s = {self._global_ok(e.cls)};") + self.emit("if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + self.emit(f"{gk} = tp_s;") + self.emit(f"if (!{gk}) {{") + self.depth += 1 + self._slow(e.cls) + if not self.f.pure: + g = self.tmp("PyObject *") + self.emit(f"{g} = tp_global(tp_dict, tp_st->names[{self.mg.name_index(e.cls)}]);") + self.emit(f"if ({g} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + self.depth -= 1 + self.emit("}") + items = [self._obj_operand(a, "New argument") for a in e.args] + t = self.tmp("PyObject *") + base = self.mg.slot_base[e.cls] + av = f"{{ PyObject *tp_av[{len(items)}] = {{{', '.join(items)}}};" if items else "{ PyObject **tp_av = NULL;" + self.emit(f"tp_s = {gk} ? {self._current('NULL', e.cls)} : 0;") + self.emit("if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + self.emit(av) + self.emit(" if (tp_s) {") + self.emit(f" {t} = tp_new_fixed((PyTypeObject *)tp_st->classes[{k}], " + f"{f'&tp_st->slots[{base}]' if items else 'NULL'}, tp_av, {len(items)});") + self.emit(" } else {") + self.depth += 1 + if self.f.pure: + self.emit(f"if ({gk}) {{ tp_class_note_slow(&tp_st->cls_rt[{k}]); }}") + self.emit("tp_rc = 1; goto tp_exit;") # (g is NULL here: pure never loads it) + else: + self.emit(f"if ({gk}) tp_class_note_slow(&tp_st->cls_rt[{k}]);") + # call what the global held when it was loaded: the class itself, or the rebinding + self.emit(f" {t} = tp_call({gk} ? tp_st->classes[{k}] : {g}, tp_av, {len(items)}, NULL);") + self.depth -= 1 + self.emit(" } }") + self._own_release(*items) # the slots took their own references + if g is not None: + self._own_release(g) + self.emit(f"if ({t} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + return t + + def _class(self, name: str) -> int: + self.mg.cls(self.f.name, name) + return self.mg.class_index[name] + def ev_Global(self, e: ir.Global) -> str: k = self.mg.name_index(e.name) return self._own_new(f"tp_global(tp_dict, tp_st->names[{k}])") @@ -852,23 +1370,36 @@ def ev_Len(self, e: ir.Len) -> str: self.emit(f"{t} = (int64_t){ptr}->len;") return t - def _slot(self, at: Type, ptr: str, index: ir.Expr, proven: bool, store: int) -> str: + def _buffer(self, name: str) -> str | None: + """`ArrayParam.buffer` of an array parameter ("array"), None for a list or local.""" + a = self.arrays.get(name) + return a.buffer if a is not None else None + + def _slot(self, at: Type, ptr: str, index: ir.Expr, proven: bool, store: int, + buffer: str | None = None) -> str: if index.type is not Type.I64: raise CGenError(f"{self.f.name}: index of type {index.type}") i = self.ev(index) if proven: return f"(Py_ssize_t)({i})" slot = self.tmp("Py_ssize_t") - self.check(f"{_ARRAY_PFX[at]}_slot({ptr}, {i}, {store}, &{slot})") + if buffer: + self.check(f"tp_buf_slot({ptr}, {i}, {store}, &{slot})") + else: + self.check(f"{_ARRAY_PFX[at]}_slot({ptr}, {i}, {store}, &{slot})") return slot def ev_Index(self, e: ir.Index) -> str: at, ptr = self._array(e.array) if e.type is not _ARRAY_ELEM[at]: raise CGenError(f"{self.f.name}: Index of {at} typed {e.type}") - slot = self._slot(at, ptr, e.index, e.proven, 0) + buffer = self._buffer(e.array) + slot = self._slot(at, ptr, e.index, e.proven, 0, buffer) t = self.tmp(_CT[e.type]) - self.emit(f"{t} = {ptr}->data[{slot}];") + if buffer: + self.emit(f"{t} = tp_buf_ld_{_BUFFER_SFX[at]}({ptr}, {slot});") + else: + self.emit(f"{t} = {ptr}->data[{slot}];") return t def ev_NewArray(self, e: ir.NewArray) -> str: @@ -893,6 +1424,54 @@ def ev_Tuple(self, e: ir.Tuple) -> str: self.emit(f"if ({t} == NULL) {{ tp_rc = -1; goto tp_exit; }}") return t + def ev_GetItem(self, e: ir.GetItem) -> str: + o = self._obj_operand(e.obj, "GetItem") # the container, then the index + k = self._obj_operand(e.index, "GetItem index") + t = self.tmp("PyObject *") + self.emit(f"{t} = tp_getitem({o}, {k});") + self._own_release(o, k) + self.emit(f"if ({t} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + return t + + def ev_Contains(self, e: ir.Contains) -> str: + a = self._obj_operand(e.left, "Contains") # CONTAINS_OP: left, then right + b = self._obj_operand(e.right, "Contains") + t = self.tmp("int") + self.emit(f"{t} = tp_contains({b}, {a});") + self._own_release(a, b) + self.emit(f"if ({t} < 0) {{ tp_rc = -1; goto tp_exit; }}") + if e.negate: + self.emit(f"{t} = !{t};") + return t + + def ev_NewList(self, e: ir.NewList) -> str: + return self._own_new("tp_new_list()") + + def ev_NewDict(self, e: ir.NewDict) -> str: + return self._own_new("tp_new_dict()") + + def ev_Format(self, e: ir.Format) -> str: + if e.conversion not in (-1, 97, 114, 115) or (e.spec is not None and "\0" in e.spec): + raise CGenError(f"{self.f.name}: Format conversion {e.conversion!r} / spec {e.spec!r}") + o = self._obj_operand(e.operand, "Format") + spec = "NULL" if e.spec is None else f"tp_st->names[{self.mg.name_index(e.spec)}]" + t = self.tmp("PyObject *") + self.emit(f"{t} = tp_format({o}, {spec}, {e.conversion});") + self._own_release(o) + self.emit(f"if ({t} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + return t + + def ev_BuildString(self, e: ir.BuildString) -> str: + if len(e.elements) < 2: + raise CGenError(f"{self.f.name}: BuildString of {len(e.elements)} elements") + items = [self._obj_operand(x, "BuildString element") for x in e.elements] + t = self.tmp("PyObject *") + self.emit(f"{{ PyObject *tp_av[{len(items)}] = {{{', '.join(items)}}};") + self.emit(f" {t} = tp_build_string(tp_av, {len(items)}); }}") + self._own_release(*items) + self.emit(f"if ({t} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + return t + def _assign_array(self, s: ir.Assign, at: Type) -> None: """`local = NewArray | CopyArray`: build into a zeroed temporary, then (only on success) free the old array and move the temporary in.""" @@ -918,6 +1497,8 @@ def _assign_array(self, s: ir.Assign, at: Type) -> None: n = self.ev(v.length) self.check(f"{pfx}_new(&{new}, {n}, {fill})") elif isinstance(v, ir.CopyArray): + if self._buffer(v.src): + raise CGenError(f"{self.f.name}: CopyArray of buffer array {v.src}") st, sptr = self._array(v.src) if st is not at: raise CGenError(f"{self.f.name}: CopyArray of {st} into {at}") @@ -960,11 +1541,74 @@ def st_StoreIndex(self, s: ir.StoreIndex) -> None: if s.value.type is not _ARRAY_ELEM[at]: raise CGenError(f"{self.f.name}: store of {s.value.type} into {at}") v = self.ev(s.value) # CPython: value, then container, then index - slot = self._slot(at, ptr, s.index, s.proven, 1) + buffer = self._buffer(s.array) + slot = self._slot(at, ptr, s.index, s.proven, 1, buffer) + if buffer: + self.emit(f"tp_buf_st_{_BUFFER_SFX[at]}({ptr}, {slot}, {v});") + return self.emit(f"{ptr}->data[{slot}] = {v};") if not local: # a local array has no list to write back to self.emit(f"{ptr}->dirty[{slot}] = 1;") + def st_StoreItem(self, s: ir.StoreItem) -> None: + v = self._obj_operand(s.value, "StoreItem value") # STORE_SUBSCR: value, container, index + o = self._obj_operand(s.obj, "StoreItem") + k = self._obj_operand(s.index, "StoreItem index") + self.emit(f"tp_s = tp_setitem({o}, {k}, {v});") + self._own_release(o, k, v) + self.emit("if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + + def st_ForObj(self, s: ir.ForObj) -> None: + if not s.targets or len(set(s.targets)) != len(s.targets) or (not s.unpack and len(s.targets) != 1): + raise CGenError(f"{self.f.name}: ForObj targets {s.targets!r} (unpack={s.unpack})") + for name in s.targets: + if self.types.get(name) is not Type.OBJ: + raise CGenError(f"{self.f.name}: ForObj target {name} must be an OBJ local") + o = self._obj_operand(s.iter, "ForObj iterable") + it = self.tmp("PyObject *") # the iterator: owned by this statement across iterations + self.emit(f"{it} = tp_get_iter({o});") # GET_ITER + self._own_release(o) + self.emit(f"if ({it} == NULL) {{ tp_rc = -1; goto tp_exit; }}") + item = self.tmp("PyObject *") + self.emit("for (;;) {") + self.depth += 1 + self.poll() + self.emit(f"{item} = tp_iter_next({it});") # FOR_ITER (StopIteration ends the loop) + self.emit(f"if ({item} == NULL) {{ if (PyErr_Occurred()) {{ tp_rc = -1; goto tp_exit; }} break; }}") + if not s.unpack: + self._own_move(_ident("l", s.targets[0]), item) + else: + n = len(s.targets) + self.emit(f"{{ PyObject *tp_u[{n}] = {{0}};") + self.emit(f" tp_s = tp_unpack({item}, {n}, tp_u);") # UNPACK_SEQUENCE; nothing owned on error + self._own_release(item) + self.emit(" if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + for i, name in enumerate(s.targets): + local = _ident("l", name) + self.emit(f" {{ PyObject *tp_old = {local}; {local} = tp_u[{i}]; tp_release(&tp_old); }}") + self.emit("}") + self.stmts(s.body) + self.depth -= 1 + self.emit("}") + self._own_release(it) # after the loop and after a `break`; a return/error exits via tp_exit + + def st_FieldSet(self, s: ir.FieldSet) -> None: + slot = self.mg.slot(self.f.name, s.cls, s.field) + v = self._obj_operand(s.value, "FieldSet value") # CPython: value, then the object + o = self._obj_operand(s.obj, "FieldSet") + self.emit(f"tp_s = {self._current(o, s.cls)};") + self.emit("if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + self.emit("if (tp_s) {") + self.emit(f" tp_s = tp_field_set({o}, {slot}, {v});") + self.emit("} else {") + self.depth += 1 + self._slow(s.cls) # FieldSet is never in a pure function + self.emit(f"tp_s = tp_setattr({o}, tp_st->names[{self.mg.name_index(s.field)}], {v});") + self.depth -= 1 + self.emit("}") + self._own_release(o, v) + self.emit("if (tp_s < 0) { tp_rc = -1; goto tp_exit; }") + def st_ExprStmt(self, s: ir.ExprStmt) -> None: v = self.ev(s.value) if v is None: @@ -992,9 +1636,29 @@ def st_If(self, s: ir.If) -> None: self.depth -= 1 self.emit("}") + @property + def snapshot(self) -> bool: + """Holds an array copy-in or an entry-globals snapshot, so must not run user code (#141).""" + return bool(self.f.entry_globals) or any(isinstance(p, ir.ArrayParam) for p in self.f.params) + + def poll(self) -> None: + """At the top of each loop iteration (#141): signals, pending calls, GIL hand-off.""" + if not self.snapshot: + # A function-local countdown (kept in a register), not the module counter: a load and + # store of a global on every iteration of a short inner loop cost fannkuch ~38%. + if self.fast: + # the poll can run a signal handler or another compiled call: count this fast tree + self.emit("if (tp_pc_tick(&tp_pc)) { tp_depth_py_begin(tp_fd); " + "tp_s = tp_poll_slow(tp_st->breaker); tp_depth_py_end(tp_fd); " + "if (tp_s != 0) { tp_rc = -1; goto tp_exit; } }") + return + self.emit("if (tp_pc_tick(&tp_pc) && tp_poll_slow(tp_st->breaker) != 0) " + "{ tp_rc = -1; goto tp_exit; }") + def st_While(self, s: ir.While) -> None: self.emit("for (;;) {") self.depth += 1 + self.poll() c = self._cond(s.cond) self.emit(f"if (!({c})) break;") self.stmts(s.body) @@ -1031,6 +1695,7 @@ def st_ForRange(self, s: ir.ForRange) -> None: self.depth += 1 self.emit(f"for ({tk} = 0; {tk} < {tn}; {tk}++) {{") self.depth += 1 + self.poll() self.emit(f"{var} = (int64_t)((uint64_t){ts} {sign} {tk} * {mag});") self.stmts(s.body) self.depth -= 1 @@ -1062,7 +1727,7 @@ def st_Continue(self, s) -> None: # whole function - def generate(self) -> str: + def impl(self) -> str: f = self.f prologue = [] for p in f.params: @@ -1077,11 +1742,16 @@ def generate(self) -> str: self.emit("tp_rc = -1;") self.emit("goto tp_exit;") - out = [_impl_signature(f), "{", + out = [_impl_signature(f, fast=self.fast), "{", " int tp_rc = 0;", " int tp_s = 0;", - " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);", - " PyObject *tp_dict = PyModule_GetDict(tp_module); /* borrowed: owned by the module */"] + " int tp_pc = TP_POLL_INTERVAL; /* loop poll countdown (#141) */", + ] + body_text = "\n".join(self.lines) + if not self.fast or "tp_st" in body_text: + out.append(" tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);") + if not self.fast or "tp_dict" in body_text: + out.append(" PyObject *tp_dict = PyModule_GetDict(tp_module); /* borrowed: owned by the module */") params = {p.name for p in f.params} | {g.name for g in f.entry_globals} for name, t in self.local_arrays.items(): out.append(f" {_ARRAY_CT[t]} {_ident('l', name)} = {{0}};") @@ -1095,23 +1765,64 @@ def generate(self) -> str: init = "NULL" if ct == "PyObject *" else "{0}" if ct in _CT_ARRAY_REV else "0" sep = "" if ct.endswith("*") else " " out.append(f" {ct}{sep}{n} = {init};") - out.append(" (void)tp_st; (void)tp_dict; (void)tp_s;" + # (a fast twin declares tp_st / tp_dict only when its body reads them: PyModule_GetState and + # PyModule_GetDict are external calls, and a leaf must stay free of them to inline well) + out.append(" (void)tp_s; (void)tp_pc;" + (" (void)tp_fd;" if self.fast else "") + + (" (void)tp_st;" if not self.fast or "tp_st" in body_text else "") + + (" (void)tp_dict;" if not self.fast or "tp_dict" in body_text else "") + "".join(f" (void){_ident('l', n)};" for n in self.types if n not in params) + "".join(f" (void){_ident('l', n)};" for n in self.local_arrays)) # Depth guard (issue #57): nothing is owned yet, so a refusal returns directly and counts # nothing; every other exit goes through tp_exit, which leaves exactly once. - out.append(" if (tp_enter_call() != 0) return -1;") + for p in f.params: + if isinstance(p, ir.Param) and p.cls is not None: + k = self._class(p.cls) + out.append(f" if ({_param_cls_fail(_ident('p', p.name), k, p.optional)}) return 1;") + k_bound = self.mg.bounded.get(f.name) + if self.fast: + # Fast twin of a bounded function (module docstring, "Fast entry"): the caller proved + # room for every frame this call tree can have, so there is nothing to count or refuse. + pass + else: + if k_bound is not None: + # Precheck: room for the k frames of this whole call tree, then run its uncounted + # twin. Without room, fall through to the counted path below, which raises + # RecursionError at exactly the call where the interpreter would. + out.append(f" if (tp_depth_room({k_bound})) {{") + out.append(" int tp_fr;") + out.append(" tp_trace_fast_hit();") + out.append(f" tp_fr = {_ident('tp_fimpl', f.name)}({_impl_call_args(f, '1')});") + out.append(" tp_depth_done();") + out.append(" return tp_fr;") + out.append(" }") + out.append(" if (tp_enter_call() != 0) return -1;") + # Eval breaker (#141): a snapshot-holding function never polls and defers others' polls. + if self.snapshot: + out.append(" tp_snapshot_depth++;") + elif not self.fast: + out.append(" if (tp_poll(tp_st->breaker) != 0) { tp_rc = -1; goto tp_exit; }") out += prologue out += self.lines out.append("tp_exit:") out += self._own_release_all() - out.append(" tp_leave_call();") + if self.snapshot: + out.append(" tp_snapshot_depth--;") + if not self.fast: + out.append(" tp_leave_call();") out.append(" return tp_rc;") out.append("}") out.append("") - out.append(self.wrapper()) return "\n".join(out) + def generate(self) -> str: + """The impl (preceded by its fast twin when bounded) and the wrapper.""" + parts = [] + if self.f.name in self.mg.bounded: + parts.append(_FnGen(self.mg, self.f, fast=True).impl()) + parts.append(self.impl()) + parts.append(self.wrapper()) + return "\n".join(parts) + def wrapper(self) -> str: f = self.f name_k = self.mg.name_index(f.name) @@ -1123,12 +1834,15 @@ def wrapper(self) -> str: init = "NULL" if f.returns is Type.OBJ else "0" L.append(f" {_CT[f.returns]} tp_r = {init};") args = [] - arrays = [] + arrays = [] # list parameters: copy-in / write-back under a critical section + buffers = [] # buffer parameters: a Py_buffer export held for the whole call (#149) + if any(isinstance(p, ir.ArrayParam) and p.buffer is None for p in f.params): + L.append(" tp_cs tp_lock = {0};") for i, p in enumerate(f.params): if isinstance(p, ir.ArrayParam): - L.append(f" {_ARRAY_CT[p.type]} tp_a{i} = {{0}};") + L.append(f" {'tp_buf' if p.buffer else _ARRAY_CT[p.type]} tp_a{i} = {{0}};") args.append(f"&tp_a{i}") - arrays.append((i, p)) + (buffers if p.buffer else arrays).append((i, p)) elif p.type is Type.OBJ: args.append(f"tp_args[{i}]") # borrowed for the call's duration else: @@ -1136,21 +1850,66 @@ def wrapper(self) -> str: args.append(f"tp_a{i}") if f.returns is not Type.NONE: args.append("&tp_r") - exits = [f" if ({_ARRAY_PFX[p.type]}_exit(&tp_a{i}) < 0) tp_x = -1;" for i, p in arrays] + # PyBuffer_Release first (cannot fail), then the lists' write-back and the critical section + exits = [f" tp_buf_exit(&tp_a{i});" for i, p in buffers] + exits += [f" if ({_ARRAY_PFX[p.type]}_exit(&tp_a{i}) < 0) tp_x = -1;" for i, p in arrays] + if arrays: + exits.append(" tp_cs_end(&tp_lock); /* after the write-back; a no-op if never taken */") + has_exits = bool(arrays or buffers) L.append(" (void)tp_x;") # arity / keywords -> the interpreted function raises CPython's own TypeError L.append(f" if (tp_nargs != {n} || (tp_kwnames != NULL && PyTuple_GET_SIZE(tp_kwnames) != 0)) goto tp_deopt;") + # fixed-layout classes: not compiled -> interpreted; Param.cls guards (before any effect) + classes = self.mg.uses(f) + cls_params = [(i, p) for i, p in enumerate(f.params) + if isinstance(p, ir.Param) and p.cls is not None] + for cname in classes: + self.mg.cls(f.name, cname) + if classes: + L.insert(3, " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);") + L.append(" if (!(" + " && ".join(f"tp_st->cls_ok[{self.mg.class_index[n]}]" + for n in classes) + ")) goto tp_deopt;") + for i, p in cls_params: + L.append(f" if ({_param_cls_fail(f'tp_args[{i}]', self._class(p.cls), p.optional)}) goto tp_deopt;") + if classes and f.entry_globals and not f.pure: + # module docstring, rule 8: a closed impure function must not reach a slow path + for cname in classes: + L.append(f" tp_s = {self._current('NULL', cname)};") + L.append(" if (tp_s < 0) goto tp_error;") + L.append(" if (!tp_s) goto tp_deopt;") + for cname in self.mg.globals_read(f): + L.append(f" tp_s = {self._global_ok(cname)};") + L.append(" if (tp_s < 0) goto tp_error;") + L.append(" if (!tp_s) goto tp_deopt;") # scalar guards: exact type (and i64 range) for i, p in enumerate(f.params): if not isinstance(p, ir.ArrayParam) and p.type in _UNBOX: L.append(f" tp_s = {_UNBOX[p.type]}(tp_args[{i}], &tp_a{i});") L.append(" if (tp_s < 0) goto tp_error;") L.append(" if (tp_s != 0) goto tp_deopt;") + # buffer arrays: exact type, then one Py_buffer export each, held until every exit runs + # `exits` (a failed guard leaves the struct zeroed, so its exit is a no-op) + for i, p in buffers: + fmt1, fmt2 = _BUFFER_FMT[p.type] + kind = _BUFFER_KINDS[p.buffer] + L.append(f" tp_s = tp_buf_enter(tp_args[{i}], " + f"((tp_state *)PyModule_GetState(tp_module))->buf_types[{kind}], " + f"{fmt1}, {fmt2}, {int(p.stored)}, &tp_a{i});") + L.append(" if (tp_s < 0) goto tp_error;") + L.append(" if (tp_s != 0) goto tp_deopt;") # aliasing guard, then copy-in if len(arrays) > 1: objs = ", ".join(f"tp_args[{i}]" for i, _ in arrays) L.append(f" {{ PyObject *const tp_objs[{len(arrays)}] = {{{objs}}};") L.append(f" if (tp_any_same(tp_objs, {len(arrays)})) goto tp_deopt; }}") + if arrays: + # Free-threaded (#159): the lists' critical section(s) from before the first copy-in to + # after the last write-back (every exit runs `exits`). A non-list argument deopts here, + # as tp_*_array_enter would, without being locked. + L.append(" if (" + " || ".join(f"!PyList_CheckExact(tp_args[{i}])" for i, _ in arrays) + + ") goto tp_deopt;") + second = f"tp_args[{arrays[1][0]}]" if len(arrays) == 2 else "NULL" + L.append(f" tp_cs_begin(&tp_lock, tp_args[{arrays[0][0]}], {second});") for i, p in arrays: L.append(f" tp_s = {_ARRAY_PFX[p.type]}_enter(tp_args[{i}], &tp_a{i});") L.append(" if (tp_s < 0) goto tp_error;") @@ -1158,7 +1917,8 @@ def wrapper(self) -> str: gnames = [] if f.entry_globals: L.insert(3, " PyObject *tp_dict = PyModule_GetDict(tp_module); /* borrowed */") - L.insert(3, " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);") + if not classes: + L.insert(3, " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);") for j, g in enumerate(f.entry_globals): # entry read of a module global: exact type or deopt (still before any effect) k = self.mg.name_index(g.name) @@ -1194,7 +1954,7 @@ def wrapper(self) -> str: else: L.append(f" return {_BOX[f.returns]}(tp_r);") L.append("tp_error:") - if arrays: + if has_exits: # keep CPython's exception; write-back still runs (partial updates stay visible) L.append(" { PyObject *tp_exc = PyErr_GetRaisedException();") L += [" " + e for e in exits] @@ -1224,13 +1984,20 @@ def wrapper(self) -> str: PyObject *tp_count = NULL, *tp_one = NULL, *tp_next = NULL, *tp_table = NULL, *tp_f = NULL; PyObject *tp_result = NULL; int tp_k; + tp_one = PyLong_FromLong(1); + if (tp_one == NULL) goto tp_done; + /* The counter is a read-modify-write of a module global: under the module dict's critical + * section, or concurrent deopts lose counts (#159). */ + tp_k = 0; + Py_BEGIN_CRITICAL_SECTION(tp_dict); tp_k = PyDict_GetItemStringRef(tp_dict, "__typedpython_deopts__", &tp_count); if (tp_k == 0) PyErr_SetString(PyExc_RuntimeError, "typedpython: __typedpython_deopts__ is missing"); + if (tp_k > 0) { + tp_next = PyNumber_Add(tp_count, tp_one); + if (tp_next == NULL || PyDict_SetItemString(tp_dict, "__typedpython_deopts__", tp_next) < 0) tp_k = -1; + } + Py_END_CRITICAL_SECTION(); if (tp_k <= 0) goto tp_done; - tp_one = PyLong_FromLong(1); - if (tp_one == NULL) goto tp_done; - tp_next = PyNumber_Add(tp_count, tp_one); - if (tp_next == NULL || PyDict_SetItemString(tp_dict, "__typedpython_deopts__", tp_next) < 0) goto tp_done; tp_k = PyDict_GetItemStringRef(tp_dict, "__typedpython_interpreted__", &tp_table); if (tp_k == 0) PyErr_SetString(PyExc_RuntimeError, "typedpython: __typedpython_interpreted__ is missing"); if (tp_k <= 0) goto tp_done; @@ -1249,29 +2016,85 @@ def wrapper(self) -> str: } ''' -_TEMPLATE_STATE = r''' -static int tp_traverse(PyObject *tp_module, visitproc visit, void *arg) -{ - tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module); - size_t i; - if (tp_st == NULL) return 0; - for (i = 0; i < sizeof(tp_st->names) / sizeof(tp_st->names[0]); i++) Py_VISIT(tp_st->names[i]); - for (i = 0; i < sizeof(tp_st->kwnames) / sizeof(tp_st->kwnames[0]); i++) Py_VISIT(tp_st->kwnames[i]); - return 0; -} +def _state_functions(mg: _ModGen) -> str: + """tp_traverse / tp_clear / tp_free of the module state. tp_clear also drops the dict watcher + (module docstring, rule 5) while the module dict still exists: CPython calls m_clear and m_free + before it releases md_dict (moduleobject.c module_clear / module_dealloc).""" + L = [ + "static int tp_traverse(PyObject *tp_module, visitproc visit, void *arg)", + "{", + " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);", + " size_t i;", + " if (tp_st == NULL) return 0;", + " Py_VISIT(tp_st->breaker);", + " for (i = 0; i < 1; i++) Py_VISIT(tp_st->buf_types[i]);", + " for (i = 0; i < sizeof(tp_st->names) / sizeof(tp_st->names[0]); i++) Py_VISIT(tp_st->names[i]);", + " for (i = 0; i < sizeof(tp_st->kwnames) / sizeof(tp_st->kwnames[0]); i++) Py_VISIT(tp_st->kwnames[i]);", + " for (i = 0; i < sizeof(tp_st->classes) / sizeof(tp_st->classes[0]); i++) Py_VISIT(tp_st->classes[i]);", + " for (i = 0; i < sizeof(tp_st->descrs) / sizeof(tp_st->descrs[0]); i++) Py_VISIT(tp_st->descrs[i]);", + " for (i = 0; i < sizeof(tp_st->cls_rt) / sizeof(tp_st->cls_rt[0]); i++) Py_VISIT(tp_st->cls_rt[i].init);", + " return 0;", + "}", + "", + "static int tp_clear(PyObject *tp_module)", + "{", + " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);", + " size_t i;", + " if (tp_st == NULL) return 0;", + " if (tp_st->watch_id1 > 0) tp_globals_unwatch(PyModule_GetDict(tp_module), &tp_st->watch_id1);", + " tp_release(&tp_st->breaker);", + " for (i = 0; i < 1; i++) tp_release(&tp_st->buf_types[i]);", + " for (i = 0; i < sizeof(tp_st->names) / sizeof(tp_st->names[0]); i++) tp_release(&tp_st->names[i]);", + " for (i = 0; i < sizeof(tp_st->kwnames) / sizeof(tp_st->kwnames[0]); i++) tp_release(&tp_st->kwnames[i]);", + " for (i = 0; i < sizeof(tp_st->cls_ok) / sizeof(tp_st->cls_ok[0]); i++) tp_st->cls_ok[i] = 0;", + " for (i = 0; i < sizeof(tp_st->classes) / sizeof(tp_st->classes[0]); i++) tp_release(&tp_st->classes[i]);", + " for (i = 0; i < sizeof(tp_st->cls_rt) / sizeof(tp_st->cls_rt[0]); i++) tp_class_rt_clear(&tp_st->cls_rt[i], NULL, 0);", + " for (i = 0; i < sizeof(tp_st->descrs) / sizeof(tp_st->descrs[0]); i++) tp_release(&tp_st->descrs[i]);", + " return 0;", + "}", + "", + "static void tp_free(void *tp_module) { (void)tp_clear((PyObject *)tp_module); }", + "", + ] + return "\n".join(L) -static int tp_clear(PyObject *tp_module) -{ - tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module); - size_t i; - if (tp_st == NULL) return 0; - for (i = 0; i < sizeof(tp_st->names) / sizeof(tp_st->names[0]); i++) tp_release(&tp_st->names[i]); - for (i = 0; i < sizeof(tp_st->kwnames) / sizeof(tp_st->kwnames[0]); i++) tp_release(&tp_st->kwnames[i]); - return 0; -} -static void tp_free(void *tp_module) { (void)tp_clear((PyObject *)tp_module); } -''' +def _class_info_function(mg: _ModGen) -> str: + """`__typedpython_class_info__()`: {class name: {compiled, captured_tag, tag, current, slow, + refreshed, watched}}, introspection of the guards (module docstring).""" + L = [ + "static PyObject *tp_class_info(PyObject *tp_module, PyObject *tp_unused)", + "{", + " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);", + " PyObject *tp_out = PyDict_New(), *tp_d;", + " PyObject *tp_c;", + " (void)tp_unused;", + " if (tp_out == NULL) return NULL;", + ] + for cd in mg.module.classes: + k = mg.class_index[cd.name] + L += [ + f" tp_c = tp_st->classes[{k}];", + " tp_d = Py_BuildValue(\"{s:O,s:I,s:I,s:O,s:n,s:n,s:O}\",", + f" \"compiled\", tp_st->cls_ok[{k}] ? Py_True : Py_False,", + f" \"captured_tag\", tp_class_rt_tag(&tp_st->cls_rt[{k}]),", + " \"tag\", tp_class_tag(tp_c),", + f" \"current\", (tp_c != NULL && tp_class_tag(tp_c) == tp_class_rt_tag(&tp_st->cls_rt[{k}])) ? Py_True : Py_False,", + f" \"slow\", tp_class_rt_slow(&tp_st->cls_rt[{k}]), \"refreshed\", " + f"tp_class_rt_refreshed(&tp_st->cls_rt[{k}]),", + " \"watched\", tp_st->watch_id1 > 0 ? Py_True : Py_False);", + f" if (tp_d == NULL || PyDict_SetItemString(tp_out, {c_string(cd.name)}, tp_d) < 0) {{", + " Py_XDECREF(tp_d); Py_DECREF(tp_out); return NULL; }", + " Py_DECREF(tp_d);", + ] + L += [ + " return tp_out;", + "}", + "", + "static PyMethodDef tp_class_info_def = {\"__typedpython_class_info__\", tp_class_info, METH_NOARGS, NULL};", + "", + ] + return "\n".join(L) def _exec_function(mg: _ModGen) -> str: @@ -1282,7 +2105,7 @@ def _exec_function(mg: _ModGen) -> str: " tp_state *tp_st = (tp_state *)PyModule_GetState(tp_module);", " PyObject *tp_dict = PyModule_GetDict(tp_module); /* borrowed: owned by the module */", " PyObject *tp_code = NULL, *tp_res = NULL, *tp_table = NULL, *tp_zero = NULL;", - " PyObject *tp_modname = NULL, *tp_f = NULL, *tp_cf = NULL;", + " PyObject *tp_modname = NULL, *tp_f = NULL, *tp_cf = NULL, *tp_c = NULL;", " int tp_rc = -1, tp_k;", " size_t i;", " for (i = 0; tp_name_strings[i] != NULL; i++) {", @@ -1304,12 +2127,50 @@ def _exec_function(mg: _ModGen) -> str: " tp_release(&tp_b);", " if (tp_k < 0) goto tp_done;", " }", + " tp_code = Py_CompileString(\"lambda: None\", \"\", Py_eval_input);", + " if (tp_code == NULL) goto tp_done;", + " tp_st->breaker = PyEval_EvalCode(tp_code, tp_dict, tp_dict);", + " tp_release(&tp_code);", + " if (tp_st->breaker == NULL) goto tp_done;", " tp_code = Py_CompileString(tp_source, tp_source_path, Py_file_input);", " if (tp_code == NULL) goto tp_done;", " tp_res = PyEval_EvalCode(tp_code, tp_dict, tp_dict);", " if (tp_res == NULL) goto tp_done;", + ] + kinds = {p.buffer for f in mg.module.functions for p in f.params + if isinstance(p, ir.ArrayParam) and p.buffer} + # buffer arrays (#149): the exact types the entry guards compare against, taken after the + # module source ran (so its own imports are done). Not importable: NULL, and every guard deopts. + if "array" in kinds: + L.append(' if (tp_import_attr("array", "array", &tp_st->buf_types[0]) < 0) goto tp_done;') + L += [ " tp_table = PyDict_New();", " if (tp_table == NULL || PyDict_SetItemString(tp_dict, \"__typedpython_interpreted__\", tp_table) < 0) goto tp_done;", + ] + for cd in mg.module.classes: + j = mg.class_index[cd.name] + L += [ + f" tp_k = PyDict_GetItemStringRef(tp_dict, {c_string(cd.name)}, &tp_c);", + " if (tp_k < 0) goto tp_done;", + " if (tp_k == 1) {", + f" tp_k = tp_class_capture(tp_c, tp_fields_{j}, {len(cd.fields)}, " + f"&tp_st->slots[{mg.slot_base[cd.name]}], &tp_st->descrs[{mg.slot_base[cd.name]}], " + f"&tp_st->cls_rt[{j}]);", + " if (tp_k < 0) goto tp_done;", + f" if (tp_k == 1) {{ tp_st->classes[{j}] = tp_c; tp_c = NULL; tp_st->cls_ok[{j}] = 1; }}", + " }", + " tp_release(&tp_c);", + ] + if mg.module.classes: + if mg.watches_globals(): + # after the source ran: from here a rebinding of a class name moves tp_globals_epoch + L.append(" tp_globals_watch(tp_dict, tp_watch_cb, &tp_st->watch_id1);") + L += [ + " tp_cf = PyCFunction_NewEx(&tp_class_info_def, tp_module, NULL);", + " if (tp_cf == NULL || PyDict_SetItemString(tp_dict, \"__typedpython_class_info__\", tp_cf) < 0) goto tp_done;", + " tp_release(&tp_cf);", + ] + L += [ " tp_zero = PyLong_FromLong(0);", " if (tp_zero == NULL || PyDict_SetItemString(tp_dict, \"__typedpython_deopts__\", tp_zero) < 0) goto tp_done;", " tp_modname = PyModule_GetNameObject(tp_module);", @@ -1327,10 +2188,15 @@ def _exec_function(mg: _ModGen) -> str: " tp_release(&tp_f); tp_release(&tp_cf);", ] L += [ + "#ifdef TP_TRACE_FAST", + f" tp_cf = PyCFunction_NewEx(&tp_methods[{len(mg.module.functions)}], tp_module, tp_modname);", + " if (tp_cf == NULL || PyDict_SetItemString(tp_dict, \"__tp_trace_fast_hits__\", tp_cf) < 0) goto tp_done;", + " tp_release(&tp_cf);", + "#endif", " tp_rc = 0;", "tp_done:", " tp_release(&tp_code); tp_release(&tp_res); tp_release(&tp_table); tp_release(&tp_zero);", - " tp_release(&tp_modname); tp_release(&tp_f); tp_release(&tp_cf);", + " tp_release(&tp_modname); tp_release(&tp_f); tp_release(&tp_cf); tp_release(&tp_c);", " return tp_rc;", "}", "", diff --git a/python-multiplatform-ksp/src/main/python/typedpython/cost.py b/python-multiplatform-ksp/src/main/python/typedpython/cost.py new file mode 100644 index 000000000..bd95614ae --- /dev/null +++ b/python-multiplatform-ksp/src/main/python/typedpython/cost.py @@ -0,0 +1,120 @@ +"""Cost policy: compile a function only if the C is expected to beat the interpreter (#150). + +This is a policy, not a proof. `verify` proves that the C for a function is safe and keeps CPython's +results; it says nothing about speed, and a function can pass every proof and still be slower +compiled. The case that made this necessary: in an *impure* function a Python `int` is a CPython +object (the front end cannot prove i64 arithmetic free of overflow where a deopt could not be +redone), so every `+`, `//`, `%` and comparison of such code is a generic `PyNumber_*` / +`PyObject_RichCompare` call on a boxed int. CPython 3.15's specialising interpreter runs exactly that +faster than the generic C calls (wordfreq `word_of`: 248.6 ms compiled against 95.7 ms interpreted). +Compiled code wins only when it does native work, so the rule asks for it. + +The rule, applied after verification (so it only ever removes verified functions, never changes +the IR): a function is kept when it is **pure**, or when some loop (`While`, `ForRange`; a `ForObj` +is a loop over objects and does not count by itself, but loops nested inside it do) has a body, nested +statements included, containing a native-typed operation: + + * a `BinOp`, `Compare`, `UnaryOp` or `MathCall` on I64/F64/BOOL operands; + * an `Index` or `StoreIndex` on an array or buffer; + * a `Call` of a compiled function with at least one argument, all of them native. + +A pure function qualifies by construction: it has no object operation at all (no `GetAttr`, +`CallObject`, OBJ `BinOp` ...), so everything it does is native, loop or not (`eval_a`, a pure +straight-line formula, must stay compiled). The rule therefore changes no pure function. + +A rejected function stays interpreted with the diagnostic below. Because an impure function can be +the target of a direct C `Call`, anything that calls a rejected function is rejected too (the same +propagation `verify` uses for an unverified callee). + +Straight-line impure functions (no loop at all) are kept: they have no loop to lose to the +interpreter, and that keeps every function that compiled before #150 compiled (fixed-layout setters, +Kotlin calls). The rule targets loops over objects only. + +The real fix is native ints in impure functions (a separate issue); when it lands, this rule is the +thing to revisit. +""" +from __future__ import annotations + +import dataclasses + +from typedpython import ir +from typedpython.ir import Type +from typedpython.verify import Diagnostic + +RULE = "policy/cost" +REASON = ("only object operations in its loops: compiled C would be slower than the interpreter " + "(#150)") + +_NATIVE = (Type.I64, Type.F64, Type.BOOL) + + +def _walk(x): + if isinstance(x, (tuple, list)): + for y in x: + yield from _walk(y) + elif dataclasses.is_dataclass(x) and not isinstance(x, type): + yield x + for f in dataclasses.fields(x): + yield from _walk(getattr(x, f.name)) + + +def _native_op(n) -> bool: + if isinstance(n, ir.BinOp): + return n.left.type in _NATIVE + if isinstance(n, ir.UnaryOp): + return n.operand.type in _NATIVE + if isinstance(n, ir.Compare): + return n.left.type in _NATIVE and n.right.type in _NATIVE + if isinstance(n, ir.MathCall): + return True + if isinstance(n, (ir.Index, ir.StoreIndex)): + return True + if isinstance(n, ir.Call): + return bool(n.args) and all(a.type in _NATIVE for a in n.args) + return False + + +def _loops(body): + for n in _walk(body): + if isinstance(n, (ir.While, ir.ForRange)): + yield n + + +def does_native_work(fn: ir.Function) -> bool: + if fn.pure: + return True + if not any(isinstance(n, (ir.While, ir.ForRange, ir.ForObj)) for n in _walk(fn.body)): + # straight-line code (a field setter, a Kotlin call): no loop to lose to the interpreter, and + # what compiled before #150 keeps compiling + return True + return any(_native_op(n) for loop in _loops(fn.body) for n in _walk(loop.body)) + + +def apply(module: ir.Module, diagnostics: list[Diagnostic]) -> tuple[ir.Module, list[Diagnostic]]: + """`module` without the functions the rule rejects (they move to `skipped`), and the diagnostics + extended by one per rejected function.""" + reasons: dict[str, str] = {} + for fn in module.functions: + if not does_native_work(fn): + reasons[fn.name] = f"{RULE}: {REASON}" + changed = True + while changed: # a direct Call needs its callee compiled + changed = False + for fn in module.functions: + if fn.name in reasons: + continue + bad = sorted({n.function for n in _walk(fn.body) + if isinstance(n, ir.Call) and n.function in reasons}) + if bad: + reasons[fn.name] = (f"{RULE}: calls '{bad[0]}', which is not compiled " + f"({reasons[bad[0]]})") + changed = True + if not reasons: + return module, diagnostics + diags = list(diagnostics) + lines = {f.name: f.source_line for f in module.functions} + for name, text in reasons.items(): + diags.append(Diagnostic(name, RULE, text.split(": ", 1)[1], lines[name])) + kept = tuple(f for f in module.functions if f.name not in reasons) + skipped = {**module.skipped, **{n: t for n, t in reasons.items()}} + return dataclasses.replace(module, functions=kept, skipped=skipped), diags diff --git a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py index 8f78ce128..fcffa0942 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py @@ -3,7 +3,7 @@ `lower(path)` reads one module and returns an `ir.Module`: - Which functions: module-level `def`s decorated with the builtin `@compiled` (a bare name, no - import — #42), or every module-level `def` when the first non-empty line is + import, #42), or every module-level `def` when the first non-empty line is `# typedpython: compiled`. - A function is lowered only when **all** of it is expressible in the IR; otherwise it is left out and `Module.skipped[name]` says why, with the line. There is no partial lowering. @@ -18,12 +18,41 @@ free of overflow is a CPython int object (OBJ, `PyNumber_*`), a call to a compiled function that may deopt goes through the module global (`CallObject(Global)`), and only `for ... in range()` targets whose every binding is such a loop stay I64. The proof is a small interval analysis: -constants, range targets (between their bounds), `len()` and the i64 range of a guarded parameter. -It is recorded in the IR: an I64 ADD/SUB/MUL/NEG it proves cannot overflow is `proven=True` (the -verifier re-proves it; a proven op does not deopt). Division and modulo of ints cannot be recorded, -so they count as deopting. An impure caller of a pure callee that may deopt and returns F64, BOOL +constants, range targets (between their bounds), `len()` (at most 2**62), the i64 range of a guarded +parameter, the join of what a plain local is assigned (a fixpoint over lowering rounds, widened +when it keeps growing), and the comparisons around a use (`if n > 0:` narrows `n` inside, `while +lo < hi:` narrows both at the top of each iteration, a returning guard narrows the code after it; +a narrowing ends at the next assignment; verify.py "Local variables and guards" is the contract and +the verifier recomputes all of it). It is recorded in the IR: an I64 ADD/SUB/MUL/NEG it proves cannot +overflow is `proven=True` (the verifier re-proves it; a proven op does not deopt), and so is an I64 +`//` or `%` by a Const power of two of a value it proves >= 0 (the back end emits a shift / mask). +Any other division or modulo of ints cannot be recorded, so it counts as deopting. An impure caller of a pure callee that may deopt and returns F64, BOOL or NONE emits `Call(redo=True)`: only the callee is redone on a deopt. +Fixed-layout classes (SPEC N-11, `ir.ClassDecl`). A module-level class (in a marked module, or +decorated `@compiled`) becomes a `ClassDecl` only when its layout is fixed in CPython too; see +`_class_decl` for the rules. A class that fails them has no `ClassDecl`, an entry in +`Module.skipped` under the class name with the reason, and the functions using it lower as before +(`GetAttr`, `CallObject`). Over a `ClassDecl`: `None` is `Const(None, OBJ)`, `is`/`is not` is +`ir.Is`, a parameter annotated `C` / `C | None` / `Optional[C]` is an OBJ `Param(cls=C, optional)`, +`obj.field` is a `FieldGet` (exact type proved: a non-optional never-rebound parameter, or a local +only ever bound to `C(...)` built as `New`; otherwise `CheckExact` in a pure function, and the +function is skipped in an impure one), `obj.field = v` is a `FieldSet` (an effect; only on a proved +object), and `C(args)` is `New` when `ClassDecl.trivial_init` (arguments in `__slots__` order) else +`CallObject(Global(C))`. `trivial_init` requires the i-th `__init__` parameter to be stored into +the i-th slot (ir.ClassDecl: "`__init__(self, *fields-in-order)`"); other orders stay an object call. + +Objects, str and dict code (#150). A function may also use, as object operations (each is the C API +call the interpreter's bytecode makes, ir.py says which): `str` constants, `{}` and `[]`, `o[i]` +(`GetItem`) and `o[i] = v` (`StoreItem`) on anything that is not a native array, `x in c` / +`x not in c` (`Contains`), f-strings (`Format` / `BuildString`, a constant format spec only), +`for t in obj` and `for a, b in obj` (`ForObj`), and augmented assignment on objects in its in-place +form. `for v in range(n)` whose bound is a CPython int object (an impure function's ints are) is a +`ForObj` over `range(n)` called as an object, and then `v` is an object too (`_ObjRange`). All of +them make the function impure and not closed, so it can hold no array and no entry global. What stays +interpreted: non-empty list/dict displays, slices, `del`, tuple-assignment, comprehensions, +`x[i] += v` on an object, a format spec with a nested field. + Entry globals (ir.Function.entry_globals). A module-level name assigned once at top level, of type float/int/bool (annotated, or literals and arithmetic of such names), never rebound or deleted in the module, and read in a *closed* function (one that runs no user code, decided after lowering) @@ -48,6 +77,8 @@ TOP = (I64_MIN, I64_MAX) EXACT_INT_FLOAT = 2**53 # |n| <= this: int/int true division is exact-rounded in C (ir.BinOp) +LEN_MAX = 2**62 # a native array is never longer (the verifier's Len interval is [0, 2**62]) + Interval = tuple[int, int] | None # None: no value at all (not yet known to be reached) TYPE_NAME = { @@ -112,6 +143,10 @@ class _ModuleInfo: bindings: dict[str, list[tuple[int, str]]] # module-level name -> [(line, how)] stdlib_math: bool scalar_globals: dict[str, Type] = field(default_factory=dict) # entry-global candidates + classes: dict[str, ir.ClassDecl] = field(default_factory=dict) # compiled fixed-layout classes + # Top-level `import x [as y]` / `from m import n [as k]` names bound exactly once in the module: + # local name -> dotted qualified name (`a` -> `array`, `array` -> `array.array`). + imports: dict[str, str] = field(default_factory=dict) def bound(self, name: str) -> bool: return name in self.bindings @@ -135,6 +170,9 @@ def lower(path: Path) -> ir.Module: info = _module_info(tree) skipped: dict[str, str] = {} + class_decls, class_skips = _classes(tree, source, info) + info.classes = class_decls + skipped.update(class_skips) signatures: dict[str, _Signature] = {} for node, reason in _targets(tree, source, info): if reason is not None: @@ -142,7 +180,7 @@ def lower(path: Path) -> ir.Module: continue assert isinstance(node, ast.FunctionDef) try: - signatures[node.name] = _signature(node) + signatures[node.name] = _signature(node, info.classes, info) except Skip as e: skipped[node.name] = e.reason @@ -173,7 +211,7 @@ def lower(path: Path) -> ir.Module: else: skipped[name] = result functions.sort(key=lambda f: f.source_line) - return ir.Module(path.stem, tuple(functions), skipped) + return ir.Module(path.stem, tuple(functions), skipped, tuple(class_decls.values())) def _module_info(tree: ast.Module) -> _ModuleInfo: @@ -205,7 +243,53 @@ def add(name: str, line: int, how: str) -> None: math = bindings.get("math", []) return _ModuleInfo(bindings, len(math) == 1 and math[0][1] == "import math", - _scalar_globals(tree, bindings)) + _scalar_globals(tree, bindings), imports=_import_names(tree, bindings)) + + +def _import_names(tree: ast.Module, bindings: dict[str, list[tuple[int, str]]]) -> dict[str, str]: + """Names bound by a plain top-level import and by nothing else in the module (so the name + certainly is that module or attribute when an annotation is evaluated).""" + out: dict[str, str] = {} + for stmt in tree.body: + if isinstance(stmt, ast.Import): + for alias in stmt.names: + if alias.asname: + out[alias.asname] = alias.name + else: + root = alias.name.split(".")[0] + out[root] = root + elif isinstance(stmt, ast.ImportFrom) and stmt.level == 0 and stmt.module: + for alias in stmt.names: + if alias.name != "*": + out[alias.asname or alias.name] = f"{stmt.module}.{alias.name}" + return {n: q for n, q in out.items() if len(bindings.get(n, [])) == 1} + + +def _qualified(node: ast.expr, info: _ModuleInfo) -> str | None: + """`a.b` -> "array.b" through the module's own imports, else None.""" + parts: list[str] = [] + while isinstance(node, ast.Attribute): + parts.append(node.attr) + node = node.value + if not isinstance(node, ast.Name) or node.id not in info.imports: + return None + return ".".join([info.imports[node.id], *reversed(parts)]) + + +def _buffer_kind(annotation: ast.expr, info: _ModuleInfo) -> tuple[Type, str] | None: + """The buffer-array reading of an annotation (#149), else None (an ordinary object parameter): + `array.array[float]` / `array.array[int]` (any spelling that resolves, through this module's + own imports, to `array.array`). A bare `array.array`, another element type, a numpy + `NDArray[...]` (not supported: its element arithmetic is numpy's), or a name bound more than + once is not one.""" + if not isinstance(annotation, ast.Subscript): + return None + base = _qualified(annotation.value, info) + arg = annotation.slice + if base == "array.array" and isinstance(arg, ast.Name) and arg.id in ("float", "int") \ + and not info.bound(arg.id): + return ARRAY_KIND[f"list[{arg.id}]"], "array" + return None def _scalar_globals(tree: ast.Module, bindings: dict[str, list[tuple[int, str]]]) -> dict[str, Type]: @@ -265,11 +349,185 @@ def _binding_targets(stmt: ast.stmt) -> list[ast.expr]: return [] +# --- fixed-layout classes (SPEC N-11) -------------------------------------------------------------- + +# A user override of any of these runs user code on a slot read, write or construction, which the +# direct slot access (FieldGet/FieldSet/New) would skip. +SLOT_ACCESS_DUNDERS = ("__getattribute__", "__getattr__", "__setattr__", "__delattr__", "__new__") + + +def _whole_module(source: str) -> bool: + return next((line.strip() for line in source.splitlines() if line.strip()), "") == MARKER + + +def is_opted_in(path: Path) -> bool: + """Whether the front end considers the module for compilation at all: the `# typedpython: compiled` + marker as its first non-empty line, or a module-level `def` / `class` decorated `@compiled` + (the same tests `_targets` and `_classes` apply).""" + source = Path(path).read_text() + if _whole_module(source): + return True + return any( + isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + and any(isinstance(d, ast.Name) and d.id == "compiled" for d in node.decorator_list) + for node in ast.parse(source, filename=str(path)).body) + + +def _classes( + tree: ast.Module, source: str, info: _ModuleInfo, +) -> tuple[dict[str, ir.ClassDecl], dict[str, str]]: + whole = _whole_module(source) + decls: dict[str, ir.ClassDecl] = {} + skipped: dict[str, str] = {} + for node in tree.body: + if not isinstance(node, ast.ClassDef): + continue + marked = any(isinstance(d, ast.Name) and d.id == "compiled" for d in node.decorator_list) + if not (whole or marked): + continue + result = _class_decl(node, tree, info) + if isinstance(result, str): + skipped[node.name] = result + else: + decls[node.name] = result + return decls, skipped + + +def _class_decl(node: ast.ClassDef, tree: ast.Module, info: _ModuleInfo) -> ir.ClassDecl | str: + """The `ClassDecl` of a class whose layout is fixed in CPython too, else why it is not one.""" + name, at = node.name, f"line {node.lineno}: class `{node.name}`" + other = [d for d in node.decorator_list if not (isinstance(d, ast.Name) and d.id == "compiled")] + if other: + return f"{at}: decorator `@{_src(other[0])}` is not supported (only `@compiled`)" + if node.decorator_list and info.bound("compiled"): + return f"{at}: `compiled` is bound in this module, so `@compiled` is not the builtin" + if node.keywords: + return f"{at}: has a metaclass or class keyword `{node.keywords[0].arg}`" + bases = [b for b in node.bases if not (isinstance(b, ast.Name) and b.id == "object")] + if bases or (node.bases and info.bound("object")): + shown = _src(bases[0]) if bases else "object" + return f"{at}: has a base class `{shown}` (only `object` is allowed)" + if len(info.bindings.get(name, [])) > 1: + lines = ", ".join(str(line) for line, _ in info.bindings[name]) + return f"{at}: `{name}` is bound more than once (lines {lines})" + for n in ast.walk(tree): + if isinstance(n, ast.Name) and isinstance(n.ctx, ast.Del) and n.id == name: + return f"{at}: `{name}` is deleted (line {n.lineno}); bound more than once" + if isinstance(n, ast.NamedExpr) and isinstance(n.target, ast.Name) and n.target.id == name: + return f"{at}: `{name}` is rebound by `:=` (line {n.lineno}); bound more than once" + if isinstance(n, ast.ClassDef) and any(isinstance(b, ast.Name) and b.id == name + for b in n.bases): + return f"{at}: is subclassed by `{n.name}` (line {n.lineno})" + for target in _assigned_attributes(n): + if isinstance(target.value, ast.Name) and target.value.id == name: + return (f"{at}: its class attribute `{target.attr}` is assigned at line " + f"{target.lineno}") + if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) \ + and n.func.id in ("setattr", "delattr") and n.args \ + and isinstance(n.args[0], ast.Name) and n.args[0].id == name: + return f"{at}: its class attribute is assigned by `{_src(n)}` at line {n.lineno}" + + slots: ast.expr | None = None + annotated: list[str] = [] + init: ast.FunctionDef | None = None + for st in node.body: + if isinstance(st, ast.Expr) and isinstance(st.value, ast.Constant) \ + and isinstance(st.value.value, str): + continue + if isinstance(st, ast.Pass): + continue + if isinstance(st, ast.Assign) and len(st.targets) == 1 \ + and isinstance(st.targets[0], ast.Name) and st.targets[0].id == "__slots__": + slots = st.value + elif isinstance(st, ast.AnnAssign) and isinstance(st.target, ast.Name) and st.value is None: + annotated.append(st.target.id) + elif isinstance(st, ast.FunctionDef): + if st.name in SLOT_ACCESS_DUNDERS: + return f"{at}: defines `{st.name}`, which runs user code on slot access" + if st.name == "__init__": + init = st + else: + return (f"{at}: class body statement `{_src(st).splitlines()[0]}` (line {st.lineno}) " + "is not supported (only `__slots__`, bare field annotations and methods)") + if slots is None: + return f"{at}: has no `__slots__`" + if not (isinstance(slots, (ast.Tuple, ast.List)) and all( + isinstance(e, ast.Constant) and isinstance(e.value, str) for e in slots.elts)): + return f"{at}: `__slots__` is not a literal tuple or list of strings" + fields = tuple(e.value for e in slots.elts) # type: ignore[attr-defined] + twice = next((f for f in fields if fields.count(f) > 1), None) + if twice is not None: + return f"{at}: `__slots__` lists `{twice}` twice" + if set(fields) != set(annotated): + return (f"{at}: `__slots__` {fields} does not equal its annotated fields " + f"{tuple(dict.fromkeys(annotated))}") + return ir.ClassDecl(name, fields, init is not None and _trivial_init(init, fields)) + + +def _assigned_attributes(n: ast.AST) -> list[ast.Attribute]: + targets: list[ast.expr] = [] + if isinstance(n, ast.Assign): + targets = list(n.targets) + elif isinstance(n, (ast.AugAssign, ast.AnnAssign)): + targets = [n.target] + elif isinstance(n, ast.Delete): + targets = list(n.targets) + elif isinstance(n, (ast.For, ast.AsyncFor)): + targets = [n.target] + found: list[ast.Attribute] = [] + for t in targets: + found += [x for x in ast.walk(t) if isinstance(x, ast.Attribute)] + return found + + +def _trivial_init(init: ast.FunctionDef, fields: tuple[str, ...]) -> bool: + """`__init__(self, p0, p1, ...)` whose body is exactly `self. = ` once per slot, with + parameter i stored into slot i (`ir.ClassDecl.trivial_init`). Statement order is free.""" + a = init.args + if init.decorator_list or a.posonlyargs or a.kwonlyargs or a.vararg or a.kwarg or a.defaults \ + or init.returns is not None and _src(init.returns) != "None" or not a.args: + return False + self_name, params = a.args[0].arg, [x.arg for x in a.args[1:]] + body = list(init.body) + if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) \ + and isinstance(body[0].value.value, str): + body = body[1:] + if len(params) != len(fields) or len(set(params)) != len(params) or len(body) != len(fields): + return False + stored: dict[str, str] = {} + for st in body: + if not (isinstance(st, ast.Assign) and len(st.targets) == 1 + and isinstance(st.targets[0], ast.Attribute) + and isinstance(st.targets[0].value, ast.Name) + and st.targets[0].value.id == self_name and isinstance(st.value, ast.Name) + and st.value.id in params and self_name not in params): + return False + field_name = st.targets[0].attr + if field_name not in fields or field_name in stored: + return False + stored[field_name] = st.value.id + return all(stored.get(f) == p for f, p in zip(fields, params)) + + +def _class_of(kind: str, classes: dict[str, ir.ClassDecl]) -> tuple[str, bool] | None: + """`C`, `C | None`, `None | C`, `Optional[C]`, `"C | None"` (an annotation spelled by + `_annotation_kind`) -> (C, optional) for a compiled class C, else None.""" + kind = kind.strip("'\"") + optional = False + for prefix in ("Optional[", "typing.Optional["): + if kind.startswith(prefix) and kind.endswith("]"): + kind, optional = kind[len(prefix):-1], True + parts = [p.strip("'\"") for p in kind.split("|")] + named = [p for p in parts if p != "None"] + if len(named) != 1 or named[0] not in classes: + return None + return named[0], optional or len(named) != len(parts) + + def _targets( tree: ast.Module, source: str, info: _ModuleInfo, ) -> list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None]]: - first = next((line.strip() for line in source.splitlines() if line.strip()), "") - whole_module = first == MARKER + whole_module = _whole_module(source) found: list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None]] = [] for node in tree.body: if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): @@ -294,7 +552,8 @@ def _targets( return found -def _signature(fn: ast.FunctionDef) -> _Signature: +def _signature(fn: ast.FunctionDef, classes: dict[str, ir.ClassDecl] | None = None, + info: _ModuleInfo | None = None) -> _Signature: args = fn.args line = fn.lineno if args.posonlyargs: @@ -312,10 +571,22 @@ def _signature(fn: ast.FunctionDef) -> _Signature: if a.annotation is None: raise Skip(a.lineno, f"parameter `{a.arg}` has no annotation") kind = _annotation_kind(a.annotation) - if kind in ARRAY_KIND: + buffer = _buffer_kind(a.annotation, info) if info is not None else None + if buffer is not None: + params.append(ir.ArrayParam(a.arg, buffer[0], False, buffer[1])) # `stored` set later + elif kind in ARRAY_KIND: params.append(ir.ArrayParam(a.arg, ARRAY_KIND[kind], False)) # `stored` set later + elif (c := _class_of(kind, classes or {})) is not None: + params.append(ir.Param(a.arg, Type.OBJ, c[0], c[1])) else: params.append(ir.Param(a.arg, SCALAR_KIND.get(kind, Type.OBJ))) + n_lists = sum(isinstance(p, ir.ArrayParam) and p.buffer is None for p in params) + if n_lists > 2: + # Free-threaded CPython (#159): the lists are locked from copy-in to write-back, and CPython + # offers one- and two-object critical sections only (a nested third could suspend the others). + raise Skip(line, f"more than two list parameters ({n_lists}) are not supported: compiled code " + "holds each list's lock from copy-in to write-back, and free-threaded CPython " + "has no lock for more than two objects at once") if fn.returns is None: raise Skip(line, "no return annotation") if isinstance(fn.returns, ast.Constant) and fn.returns.value is None: @@ -393,7 +664,12 @@ def _lower_function( try: return _lower_in_mode(sig, env, info, inferred, impure=True) except Skip as e: - return (pure_error or e).reason + # The pure attempt's reason is the natural one, unless it stopped after an impure construct: + # then the function was never going to be pure and the pure mode's types (a native int where + # the impure mode has an object) make its complaint beside the point. + if pure_error is not None and not getattr(pure_error, "impure_seen", False): + return pure_error.reason + return e.reason def _lower_in_mode(sig, env, info, inferred, impure: bool) -> ir.Function: @@ -405,21 +681,52 @@ def _lower_in_mode(sig, env, info, inferred, impure: bool) -> ir.Function: return fn +class _ObjRange(Exception): + """A `for v in range(...)` whose bound is a CPython int object: `v` cannot be an I64 range + index. Raised once per variable; the function is lowered again with `v` an object (a `ForObj`).""" + + def __init__(self, var: str) -> None: + super().__init__(var) + self.var = var + + def _lower_with_bounds(sig, env, info, inferred, impure: bool, candidates: dict[str, Type]) -> ir.Function: + obj_ranges: frozenset[str] = frozenset() + while True: + try: + return _lower_rounds(sig, env, info, inferred, impure, candidates, obj_ranges) + except _ObjRange as r: + assert r.var not in obj_ranges, "an object range variable was asked for twice" + obj_ranges |= {r.var} + + +def _lower_rounds(sig, env, info, inferred, impure: bool, candidates: dict[str, Type], + obj_ranges: frozenset[str]) -> ir.Function: # Range-target intervals: least fixpoint by iteration, widened to TOP if it does not settle. + # A local that keeps growing (a counter) is widened to the i64 limit from round 3 on, as the + # verifier's own fixpoint does (verify._prove). bounds: dict[str, Interval] = {} - for _ in range(8): - lowerer = _Lowerer(sig, env, info, inferred, impure, bounds, candidates) - fn = lowerer.run() + for rnd in range(16): + lowerer = _Lowerer(sig, env, info, inferred, impure, bounds, candidates, obj_ranges) + try: + fn = lowerer.run() + except Skip as e: + e.impure_seen = lowerer.impure # type: ignore[attr-defined] + raise joined = {**bounds} for var, iv in lowerer.found_bounds.items(): - joined[var] = _join(joined.get(var), iv) + new = _join(joined.get(var), iv) + old = joined.get(var) + if rnd >= 3 and var in lowerer.acc_vars and old is not None and new != old: + new = (old[0] if new[0] >= old[0] else I64_MIN, + old[1] if new[1] <= old[1] else I64_MAX) + joined[var] = new if joined == bounds: return fn bounds = joined widened = {var: TOP for var in bounds} - return _Lowerer(sig, env, info, inferred, impure, widened, candidates).run() + return _Lowerer(sig, env, info, inferred, impure, widened, candidates, obj_ranges).run() def _join(a: Interval, b: Interval) -> Interval: @@ -439,8 +746,10 @@ class _Binding: class _Lowerer: def __init__(self, sig: _Signature, env: dict[str, ir.Function], info: _ModuleInfo, inferred: dict[str, list[tuple[int, str]]], impure: bool, - bounds: dict[str, Interval], candidates: dict[str, Type]) -> None: + bounds: dict[str, Interval], candidates: dict[str, Type], + obj_ranges: frozenset[str] = frozenset()) -> None: self.sig = sig + self.obj_ranges = obj_ranges self.candidates = candidates self.entry_used: dict[str, Type] = {} self.fn = sig.node @@ -462,8 +771,22 @@ def __init__(self, sig: _Signature, env: dict[str, ir.Function], info: _ModuleIn self.range_vars = { n for n, bs in self.bindings.items() if n not in self.params and any(b.how == "range" for b in bs) - and all(b.how in ("range", "annotation") for b in bs) + and all(b.how in ("range", "annotation") for b in bs) and n not in self.obj_ranges } + # Locals bound by a loop over an object (`for t in obj`, an object range): always objects. + self.iter_vars = {n for n, bs in self.bindings.items() if any(b.how == "for" for b in bs)} + self.iter_vars |= self.obj_ranges + # Locals only ever assigned by plain `=` / `+=` statements (never a loop target): their + # interval is the join of what is assigned to them (found_bounds, widened across rounds). + self.acc_vars = { + n for n, bs in self.bindings.items() + if n not in self.params and n not in self.local_arrays + and any(b.how == "assign" for b in bs) + and all(b.how in ("assign", "annotation") for b in bs) + } + # Guard facts at this point (verify.py "Local variables and guards"): local -> interval. + self.refined: dict[str, tuple[int, int]] = {} + self.dead = 0 # >0: in code whose guard read a local with no interval yet in this round self.all_names = {n.id for n in ast.walk(self.fn) if isinstance(n, ast.Name)} \ | set(self.params) @@ -475,6 +798,7 @@ def __init__(self, sig: _Signature, env: dict[str, ir.Function], info: _ModuleIn self.deopts = False # a node that can deopt self.stored: set[str] = set() self.current: ast.stmt | None = None # the statement being lowered (for messages) + self._new_locals: dict[str, str] | None = None # --- entry --- @@ -491,7 +815,8 @@ def run(self) -> ir.Function: locals_[name] = self.local_type(name, self.bindings[name][0].line) locals_.update(self.temps) params = tuple( - ir.ArrayParam(p.name, p.type, p.name in self.stored) if isinstance(p, ir.ArrayParam) + ir.ArrayParam(p.name, p.type, p.name in self.stored, p.buffer) + if isinstance(p, ir.ArrayParam) else p for p in self.sig.params ) pure = not self.impure @@ -543,6 +868,13 @@ def _find_local_arrays(self) -> dict[str, Type]: kind = _creation_kind(value) if kind is not None and name not in self.params: creations.setdefault(name, []).append((stmt, kind, value)) + buffers = {p.name for p in self.sig.params if isinstance(p, ir.ArrayParam) and p.buffer} + for cs in creations.values(): + for stmt, kind, value in cs: + if kind == "copy" and value.value.id in buffers: # type: ignore[attr-defined] + raise Skip(stmt.lineno, f"`{_src(value)}` slices buffer parameter " + f"`{value.value.id}`: a buffer array is never copied " # type: ignore[attr-defined] + "(`[:]` would be a native copy of a list)") known = set(self.arrays) | {n for n, cs in creations.items() if any(k != "copy" for _, k, _ in cs)} while True: @@ -605,6 +937,9 @@ def local_type(self, name: str, line: int) -> Type: if name not in self.annotations: kinds += self.inferred.get(name, []) distinct = sorted({k for _, k in kinds}) + if not distinct and name in self.iter_vars: + self.types[name] = Type.OBJ # what a loop over an object binds: an object + return Type.OBJ if not distinct: raise Skip(self.bindings[name][0].line, f"no type is known for local `{name}`") if len(distinct) > 1: @@ -619,6 +954,9 @@ def local_type(self, name: str, line: int) -> Type: t = Type.OBJ if self.impure_mode and name not in self.range_vars else Type.I64 else: t = SCALAR_KIND.get(kind, Type.OBJ) + if name in self.iter_vars and t != Type.OBJ: + raise Skip(self.bindings[name][0].line, f"loop variable `{name}` is bound by a loop over " + f"an object but is a native {TYPE_NAME[t]}") self.types[name] = t return t @@ -677,13 +1015,41 @@ def stmt(self, node: ast.stmt) -> list[ir.Stmt]: if isinstance(node, ast.If): cond = self.condition(node.test) pre = self.take() - return pre + [ir.If(cond, self.block(node.body), self.block(node.orelse))] + entry = self.refined + self.refined, dead = self.narrowed(cond, True, entry) + self.dead += dead + then = self.block(node.body) + self.dead -= dead + then_refined = self.refined + self.refined, dead = self.narrowed(cond, False, entry) + self.dead += dead + orelse = self.block(node.orelse) + self.dead -= dead + else_refined = self.refined + then_ends, else_ends = _diverges(then), _diverges(orelse) + if then_ends and else_ends: + self.refined = entry + elif then_ends: + self.refined = else_refined + elif else_ends: + self.refined = then_refined + else: + self.refined = {k: (min(v[0], else_refined[k][0]), max(v[1], else_refined[k][1])) + for k, v in then_refined.items() if k in else_refined} + return pre + [ir.If(cond, then, orelse)] if isinstance(node, ast.While): if node.orelse: raise Skip(line, "`while ... else` is not supported") + killed = _stored_names(node.body) + head = {k: v for k, v in self.refined.items() if k not in killed} + self.refined = head cond = self.condition(node.test) pre = self.take() + self.refined, dead = self.narrowed(cond, True, head) + self.dead += dead body = self.block(node.body) + self.dead -= dead + self.refined = dict(head) if not pre: return [ir.While(cond, body)] # The condition needs statements (a chain's temporary): run them on every iteration. @@ -705,8 +1071,19 @@ def assign(self, target: ast.expr, value_node: ast.expr, node: ast.stmt) -> list raise Skip(line, f"array parameter `{name}` is rebound") value = self.value(value_node) t = self.local_type(name, line) - return self.take() + [ir.Assign(name, self.coerce(value, t, value_node, - f"local `{name}`"))] + value = self.coerce(value, t, value_node, f"local `{name}`") + self.note_assign(name, value) + return self.take() + [ir.Assign(name, value)] + if isinstance(target, ast.Subscript) and not ( + isinstance(target.value, ast.Name) and target.value.id in self.arrays): + if isinstance(target.slice, ast.Slice): + raise Skip(line, f"slice assignment `{_src(target)} = ...` is not supported") + # STORE_SUBSCR: the value, then the container, then the index. + value = self.boxed(self.value(value_node), value_node) + obj = self.boxed(self.sub(target.value), target.value) + key = self.boxed(self.sub(target.slice), target.slice) + self.impure = True + return self.take() + [ir.StoreItem(obj, key, value)] if isinstance(target, ast.Subscript): array, elem = self.array_of(target) # CPython evaluates the value first, then the index. @@ -718,11 +1095,123 @@ def assign(self, target: ast.expr, value_node: ast.expr, node: ast.stmt) -> list self.store_effect(array) return self.take() + [ir.StoreIndex(array, index, value)] if isinstance(target, ast.Attribute): + found = self.field_set(target, value_node) + if found is not None: + return found raise Skip(line, f"assignment to attribute `{_src(target)}` is not supported") if isinstance(target, (ast.Tuple, ast.List)): raise Skip(line, "tuple unpacking is not supported") raise _unsupported(target) + # --- fixed-layout classes --- + + def new_class(self, node: ast.expr) -> str | None: + """The class C when `node` is `C(args)` that lowers to `ir.New`.""" + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)): + return None + decl = self.info.classes.get(node.func.id) + if decl is None or not decl.trivial_init or self.is_local(decl.name) \ + or node.keywords or any(isinstance(a, ast.Starred) for a in node.args) \ + or len(node.args) != len(decl.fields): + return None + return decl.name + + def new_locals(self) -> dict[str, str]: + """Locals every one of whose bindings is a `C(...)` lowered to `New`: always exactly a C.""" + if self._new_locals is None: + good: dict[str, set[str]] = {} + bad: set[str] = set() + for stmt in rebinding._own_statements(self.fn): + if stmt is self.fn: + continue + value = None + if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 \ + and isinstance(stmt.targets[0], ast.Name): + name, value = stmt.targets[0].id, stmt.value + elif isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + name, value = stmt.target.id, stmt.value + if value is None: + continue + else: + for t in _binding_targets(stmt): + bad |= {n.id for n in ast.walk(t) if isinstance(n, ast.Name)} + continue + c = self.new_class(value) + if c is None: + bad.add(name) + else: + good.setdefault(name, set()).add(c) + self._new_locals = {n: next(iter(cs)) for n, cs in good.items() + if n not in bad and len(cs) == 1 and n not in self.params} + return self._new_locals + + def cls_of_name(self, name: str) -> tuple[str | None, bool]: + """(compiled class the name is declared to hold, whether its exact type is proved).""" + classes = self.info.classes + if name in self.params: + p = self.params[name] + if isinstance(p, ir.Param) and p.cls is not None: + rebound = any(b.how != "annotation" for b in self.bindings.get(name, [])) + return p.cls, not p.optional and not rebound + return None, False + if name in self.new_locals(): + return self.new_locals()[name], True + if name in self.bindings: + kinds = list(self.annotations.get(name, [])) + if name not in self.annotations: + kinds += self.inferred.get(name, []) + found = {_class_of(k, classes) for _, k in kinds} + if len(found) == 1 and None not in found: + return next(iter(found))[0], False # type: ignore[index] + return None, False + + def field_target(self, node: ast.Attribute) -> tuple[ir.Local, str, bool] | None: + """(object, class, exact type proved) for `name.field` of a compiled class's field.""" + if not (isinstance(node.value, ast.Name) and self.is_local(node.value.id)): + return None + cls, exact = self.cls_of_name(node.value.id) + if cls is None or node.attr not in self.info.classes[cls].fields: + return None + return ir.Local(Type.OBJ, node.value.id), cls, exact + + def field_get(self, node: ast.Attribute) -> ir.Expr | None: + found = self.field_target(node) + if found is None: + return None + obj, cls, exact = found + if not exact: + if self.impure_mode: + raise Skip(node.lineno, f"`{_src(node)}`: the exact type of `{obj.name}` is not " + f"proved to be `{cls}`, and an impure function cannot " + "deopt on a failed check") + obj = ir.CheckExact(Type.OBJ, obj, cls) + self.deopts = True + return ir.FieldGet(Type.OBJ, obj, cls, node.attr) + + def field_set(self, target: ast.Attribute, value_node: ast.expr) -> list[ir.Stmt] | None: + found = self.field_target(target) + if found is None: + return None + obj, cls, exact = found + if not exact: + raise Skip(target.lineno, f"`{_src(target)} = ...`: the exact type of `{obj.name}` is " + f"not proved to be `{cls}`, and a store cannot be guarded by " + "a deopting check") + value = self.boxed(self.value(value_node), value_node) + self.impure = True + return self.take() + [ir.FieldSet(obj, cls, target.attr, value)] + + def arg_proved(self, e: ir.Expr, p: ir.Param) -> bool: + """The argument `e` is certainly accepted by the callee's entry guard for `p.cls`.""" + if isinstance(e, ir.Const) and e.value is None: + return p.optional + if isinstance(e, (ir.New, ir.CheckExact)): + return e.cls == p.cls + if isinstance(e, ir.Local) and e.type == Type.OBJ: + cls, exact = self.cls_of_name(e.name) + return exact and cls == p.cls + return False + def array_word(self, name: str) -> str: return "local array" if name in self.local_arrays else "array parameter" @@ -782,8 +1271,10 @@ def aug_assign(self, node: ast.AugAssign) -> list[ir.Stmt]: raise _unsupported(node) t = self.local_type(name, line) current = ir.Local(t, name) - result = self.arith(op, current, self.sub(node.value), node) - return self.take() + [ir.Assign(name, self.coerce(result, t, node, f"local `{name}`"))] + result = self.arith(op, current, self.sub(node.value), node, inplace=True) + result = self.coerce(result, t, node, f"local `{name}`") + self.note_assign(name, result) + return self.take() + [ir.Assign(name, result)] if isinstance(target, ast.Subscript): array, elem = self.array_of(target) index = self.index(target.slice) @@ -799,19 +1290,56 @@ def aug_assign(self, node: ast.AugAssign) -> list[ir.Stmt]: raise Skip(line, f"assignment to attribute `{_src(target)}` is not supported") raise _unsupported(target) + def for_object(self, node: ast.For, call: ast.Call | None = None) -> list[ir.Stmt]: + """`for t in obj` / `for a, b in obj` (ir.ForObj); `call` is an object-bounded `range(...)`.""" + line = node.lineno + target = node.target + if isinstance(target, ast.Name): + names, unpack = [target.id], False + elif isinstance(target, (ast.Tuple, ast.List)) and target.elts \ + and all(isinstance(e, ast.Name) for e in target.elts): + names, unpack = [e.id for e in target.elts], True # type: ignore[attr-defined] + else: + raise Skip(line, f"loop target `{_src(target)}` is not supported (names only)") + if len(set(names)) != len(names): + raise Skip(line, f"loop target `{_src(target)}` binds a name twice") + for n in names: + if n in self.arrays: + raise Skip(line, f"loop variable `{n}` is an array") + if self.local_type(n, line) != Type.OBJ: + raise Skip(line, f"loop variable `{n}` is a native {TYPE_NAME[self.local_type(n, line)]}" + ", but a loop over an object binds objects") + if call is not None: # range(...) with an object bound: called as the object it is + callee = ir.Global(Type.OBJ, "range") + self.hoist_ok = False + it: ir.Expr = ir.CallObject(Type.OBJ, callee, tuple( + self.boxed(self.value(a), a) for a in call.args)) + else: + it = self.boxed(self.value(node.iter), node.iter) + self.impure = True + pre = self.take() + killed = _stored_names(node.body) | set(names) + head = {k: v for k, v in self.refined.items() if k not in killed} + self.refined = head + body = self.block(node.body) + self.refined = dict(head) + return pre + [ir.ForObj(tuple(names), it, body, unpack)] + def for_range(self, node: ast.For) -> list[ir.Stmt]: line = node.lineno if node.orelse: raise Skip(line, "`for ... else` is not supported") - if not isinstance(node.target, ast.Name): - raise Skip(line, f"loop target `{_src(node.target)}` is not supported (only a name)") if isinstance(node.iter, ast.Name) and node.iter.id in self.arrays: raise self.array_misuse(node.iter.id, node.iter) if not (_is_range_call(node.iter) and self.builtin("range")): - raise Skip(line, f"`for` over `{_src(node.iter)}` is not supported (only range())") + return self.for_object(node) call = node.iter assert isinstance(call, ast.Call) + if not isinstance(node.target, ast.Name): + raise Skip(line, f"loop target `{_src(node.target)}` is not supported (only a name)") var = node.target.id + if var in self.obj_ranges: + return self.for_object(node, call) if var in self.params: raise Skip(line, f"loop variable `{var}` is a parameter") for inner in ast.walk(ast.Module(body=node.body, type_ignores=[])): @@ -832,25 +1360,31 @@ def for_range(self, node: ast.For) -> list[ir.Stmt]: step = 1 if len(args) == 1: start: ir.Expr = ir.Const(Type.I64, 0) - stop = self.range_bound(args[0]) + stop = self.range_bound(args[0], var) else: - start = self.range_bound(args[0]) - stop = self.range_bound(args[1]) + start = self.range_bound(args[0], var) + stop = self.range_bound(args[1], var) pre = self.take() s, e = self.interval(start), self.interval(stop) - if s is not None and e is not None: + if s is not None and e is not None and not self.dead: if step > 0: iv = (s[0], max(s[0], e[1] - 1)) else: iv = (min(s[1], e[0] + 1), s[1]) self.found_bounds[var] = _join(self.found_bounds.get(var), iv) + killed = _stored_names(node.body) | {var} + head = {k: v for k, v in self.refined.items() if k not in killed} + self.refined = head body = self.block(node.body) + self.refined = dict(head) return pre + [ir.ForRange(var, start, stop, ir.Const(Type.I64, step), body)] - def range_bound(self, node: ast.expr) -> ir.Expr: + def range_bound(self, node: ast.expr, var: str | None = None) -> ir.Expr: e = self.value(node) + if e.type == Type.OBJ and self.impure_mode and var is not None: + raise _ObjRange(var) # a CPython int object: the loop is a ForObj over range(...) if e.type != Type.I64: why = (" (it is a CPython int here: its arithmetic is not proved free of i64 overflow " "in an impure function)" if e.type == Type.OBJ and self.impure_mode else "") @@ -860,6 +1394,8 @@ def range_bound(self, node: ast.expr) -> ir.Expr: def return_(self, node: ast.Return) -> list[ir.Stmt]: returns = self.sig.returns if node.value is None or (isinstance(node.value, ast.Constant) and node.value.value is None): + if returns == Type.OBJ: + return [ir.Return(ir.Const(Type.OBJ, None))] if returns != Type.NONE: raise Skip(node.lineno, f"returns None from a function declared to return " f"{TYPE_NAME[returns]}") @@ -940,6 +1476,10 @@ def expr(self, node: ast.expr, cond: int = 0) -> ir.Expr: return ir.Const(Type.I64, v) if isinstance(v, float): return ir.Const(Type.F64, v) + if v is None: + return ir.Const(Type.OBJ, None) + if isinstance(v, str): + return self.str_const(v, node) raise Skip(line, f"{type(v).__name__} constant `{_src(node)}` is not supported") if isinstance(node, ast.Name): return self.name(node) @@ -959,6 +1499,9 @@ def expr(self, node: ast.expr, cond: int = 0) -> ir.Expr: if isinstance(node, ast.Attribute): if isinstance(node.value, ast.Name) and node.value.id in self.arrays: raise self.array_misuse(node.value.id, node) + field_get = self.field_get(node) + if field_get is not None: + return field_get obj = self.boxed(self.sub(node.value), node.value) self.impure = True return ir.GetAttr(Type.OBJ, obj, node.attr) @@ -970,9 +1513,55 @@ def expr(self, node: ast.expr, cond: int = 0) -> ir.Expr: "right-hand side of an assignment is a native copy") array, elem = self.array_of(node) return ir.Index(elem, array, self.index(node.slice)) - raise Skip(line, f"subscript `{_src(node)}` of an object is not supported") + if isinstance(node.slice, ast.Slice): + raise Skip(line, f"slice `{_src(node)}` is not supported") + obj = self.boxed(self.sub(node.value), node.value) # the container, then the index + key = self.boxed(self.sub(node.slice), node.slice) + self.impure = True + return ir.GetItem(Type.OBJ, obj, key) + if isinstance(node, ast.Dict) and not node.keys: + return ir.NewDict(Type.OBJ) + if isinstance(node, ast.List) and not node.elts and isinstance(node.ctx, ast.Load): + return ir.NewList(Type.OBJ) + if isinstance(node, ast.JoinedStr): + return self.fstring(node) raise _unsupported(node) + def str_const(self, v: str, node: ast.AST) -> ir.Expr: + """A `str` constant: the text must be exactly the UTF-8 C string the back end embeds.""" + try: + v.encode("utf-8") + except UnicodeEncodeError: + raise Skip(node.lineno, "a str constant with a lone surrogate is not supported") from None + if "\0" in v: + raise Skip(node.lineno, "a str constant containing NUL is not supported") + return ir.Const(Type.OBJ, v) + + def fstring(self, node: ast.JoinedStr) -> ir.Expr: + """BUILD_STRING of the constant pieces and the `Format` of each field, in source order.""" + parts: list[ir.Expr] = [] + for piece in node.values: + if isinstance(piece, ast.Constant) and isinstance(piece.value, str): + parts.append(self.str_const(piece.value, piece)) + continue + assert isinstance(piece, ast.FormattedValue) + spec = None + if piece.format_spec is not None: + texts = piece.format_spec.values # type: ignore[attr-defined] + if not all(isinstance(t, ast.Constant) and isinstance(t.value, str) for t in texts): + raise Skip(piece.lineno, f"format spec of `{_src(piece)}` has a nested field " + "(only a constant spec is supported)") + spec = "".join(t.value for t in texts) # type: ignore[attr-defined] + self.str_const(spec, piece) + operand = self.boxed(self.value(piece.value), piece.value) + self.impure = True + parts.append(ir.Format(Type.OBJ, operand, spec, piece.conversion)) + if not parts: + return ir.Const(Type.OBJ, "") + if len(parts) == 1: + return parts[0] # the compiler emits no BUILD_STRING for one piece + return ir.BuildString(Type.OBJ, tuple(parts)) + def name(self, node: ast.Name) -> ir.Expr: name = node.id if name in self.arrays: @@ -1019,20 +1608,21 @@ def binop_kind(self, op: ast.operator, node: ast.AST) -> ir.BinOpKind: raise Skip(node.lineno, f"operator `{_src(node)}` is not supported") return kind - def arith(self, op: ir.BinOpKind, left: ir.Expr, right: ir.Expr, node: ast.AST) -> ir.Expr: + def arith(self, op: ir.BinOpKind, left: ir.Expr, right: ir.Expr, node: ast.AST, + inplace: bool = False) -> ir.Expr: lt, rt = left.type, right.type if Type.BOOL in (lt, rt) or Type.NONE in (lt, rt): raise Skip(node.lineno, f"arithmetic on {TYPE_NAME[Type.BOOL if Type.BOOL in (lt, rt) else Type.NONE]} " f"in `{_src(node)}` is not supported") if Type.OBJ in (lt, rt): - return self.obj_binop(op, left, right) + return self.obj_binop(op, left, right, inplace) if lt == Type.I64 and rt == Type.I64: result = Type.F64 if op == ir.BinOpKind.TRUEDIV else Type.I64 e = ir.BinOp(result, op, left, right) if self.recordable(e) and self.proved(e): return dataclasses.replace(e, proven=True) if self.impure_mode: - return self.obj_binop(op, left, right) + return self.obj_binop(op, left, right, inplace) self.deopts = True return e if lt == Type.I64: @@ -1041,9 +1631,10 @@ def arith(self, op: ir.BinOpKind, left: ir.Expr, right: ir.Expr, node: ast.AST) right = ir.ToFloat(Type.F64, right) return ir.BinOp(Type.F64, op, left, right) - def obj_binop(self, op: ir.BinOpKind, left: ir.Expr, right: ir.Expr) -> ir.Expr: + def obj_binop(self, op: ir.BinOpKind, left: ir.Expr, right: ir.Expr, + inplace: bool = False) -> ir.Expr: self.impure = True - return ir.BinOp(Type.OBJ, op, _box(left), _box(right)) + return ir.BinOp(Type.OBJ, op, _box(left), _box(right), inplace=inplace) def unary(self, node: ast.UnaryOp) -> ir.Expr: if isinstance(node.op, ast.Not): @@ -1101,10 +1692,19 @@ def compare(self, node: ast.Compare, cond: int) -> ir.Expr: def link(self, op: ast.cmpop, left: ir.Expr, right: ir.Expr, obj_ok: bool, node: ast.Compare) -> ir.Expr: + if isinstance(op, (ast.Is, ast.IsNot)): + if left.type != Type.OBJ or right.type != Type.OBJ: + raise Skip(node.lineno, f"identity comparison `{_src(node)}` of a native " + f"{TYPE_NAME[left.type if left.type != Type.OBJ else right.type]}" + " is not supported (only of objects)") + return ir.Is(Type.BOOL, left, right, isinstance(op, ast.IsNot)) + if isinstance(op, (ast.In, ast.NotIn)): + self.impure = True + return ir.Contains(Type.BOOL, _box(left), _box(right), isinstance(op, ast.NotIn)) kind = COMPARE.get(type(op)) if kind is None: raise Skip(node.lineno, f"comparison `{_src(node)}` is not supported " - "(only == != < <= > >=)") + "(only == != < <= > >=, in, not in, is, is not)") lt, rt = left.type, right.type if Type.OBJ in (lt, rt): if not obj_ok: @@ -1123,8 +1723,8 @@ def boolop(self, node: ast.BoolOp, cond: int) -> ir.Expr: for i, v in enumerate(node.values): if i > 0: self.hoist_ok = False # evaluated only if the earlier operands did not decide - e = self.sub(v, 1 if cond == 2 else 0) - if cond == 2: + e = self.sub(v, 1 if cond else 0) + if cond: e = self.truth(e, v) elif e.type != Type.BOOL: raise Skip(node.lineno, f"`{word}` on a non-bool operand `{_src(v)}` outside a " @@ -1161,6 +1761,11 @@ def call(self, node: ast.Call) -> ir.Expr: return arg # float(x) of an exact float is x itself self.impure = True return ir.ObjToFloat(Type.F64, _box(arg)) + new_cls = self.new_class(node) + if new_cls is not None: + self.hoist_ok = False + return ir.New(Type.OBJ, new_cls, + tuple(self.boxed(self.value(a), a) for a in node.args)) if not self.is_local(name) and name in self.env: direct = self.direct_call(name, node) if direct is not None: @@ -1200,13 +1805,19 @@ def direct_call(self, name: str, node: ast.Call) -> ir.Expr | None: for p, e in zip(callee.params, args): if p.type != Type.OBJ and e.type != p.type: direct = False # the callee's guard would deopt; the global keeps CPython's result + # A class parameter's guard (`type(x) is C`) fails for an argument not proved to be a C: + # that is a deopt of the callee at its entry, redoable only by a pure caller. + unproved = any(isinstance(p, ir.Param) and p.cls is not None and not self.arg_proved(e, p) + for p, e in zip(callee.params, args)) + if unproved and self.impure_mode: + direct = False if not direct: self.impure = True return ir.CallObject(Type.OBJ, ir.Global(Type.OBJ, name), tuple(self.boxed(e, a) for e, a in zip(args, node.args))) if not callee.pure: self.impure = True - if callee.may_deopt and not redo: + if (callee.may_deopt and not redo) or unproved: self.deopts = True typed = tuple(self.boxed(e, a) if p.type == Type.OBJ else e for p, e, a in zip(callee.params, args, node.args)) @@ -1254,18 +1865,75 @@ def coerce(self, value: ir.Expr, target: Type, node: ast.AST, what: str) -> ir.E def interval(self, e: ir.Expr) -> Interval: if isinstance(e, ir.Const) and e.type == Type.I64: return (e.value, e.value) # type: ignore[return-value] - if isinstance(e, ir.Local) and e.type == Type.I64 and e.name in self.range_vars: - return self.bounds.get(e.name) + if isinstance(e, ir.Local) and e.type == Type.I64: + base = (self.bounds.get(e.name) if e.name in self.range_vars or e.name in self.acc_vars + else TOP) + if base is None: + return None + return _meet(base, self.refined.get(e.name)) if isinstance(e, ir.Len): - return (0, I64_MAX) - if isinstance(e, (ir.BinOp, ir.UnaryOp)) and e.type == Type.I64 and self.proved(e): - return self._result_interval(e) + return (0, LEN_MAX) + if isinstance(e, (ir.BinOp, ir.UnaryOp)) and e.type == Type.I64: + if isinstance(e, ir.BinOp) and e.op in (ir.BinOpKind.FLOORDIV, ir.BinOpKind.MOD): + a, b = self.interval(e.left), self.interval(e.right) + if a is None or b is None: + return None + return _clamp(_div_interval(e.op, a, b)) + if isinstance(e, ir.BinOp) and e.op == ir.BinOpKind.TRUEDIV: + return TOP + # ADD/SUB/MUL/NEG/POS: an op that overflows deopts, so only in-range results flow on + r = self._result_interval(e) + return None if r is None else _clamp(r) return TOP - @staticmethod - def recordable(e: ir.BinOp) -> bool: - """`proven` exists for ADD/SUB/MUL only; the verifier cannot re-prove the others.""" - return e.op in (ir.BinOpKind.ADD, ir.BinOpKind.SUB, ir.BinOpKind.MUL) + def note_assign(self, name: str, value: ir.Expr) -> None: + """`name = value` was lowered: record the value's interval for the local (acc_vars) and end + any guard on it.""" + if name in self.acc_vars and not self.dead and value.type == Type.I64: + iv = self.interval(value) + if iv is not None: + self.found_bounds[name] = _join(self.found_bounds.get(name), iv) + self.refined.pop(name, None) + + def narrowed(self, cond: ir.Expr, truth: bool, base: dict[str, tuple[int, int]] + ) -> tuple[dict[str, tuple[int, int]], bool]: + """(`base` narrowed by `cond` being `truth`, whether a bound read a local with no interval + yet). Same rules as verify._Checker.refinements.""" + self.refined = base + found, bottom = {}, False + stack = [(cond, truth)] + while stack: + c, t = stack.pop() + if isinstance(c, ir.UnaryOp) and c.op == ir.UnaryOpKind.NOT: + stack.append((c.operand, not t)) + elif isinstance(c, (ir.And, ir.Or)) and (isinstance(c, ir.And) == t): + stack += [(c.left, t), (c.right, t)] + elif isinstance(c, ir.Compare) and c.left.type == Type.I64 and c.right.type == Type.I64: + kind = c.op if t else _NEGATE[c.op] + for local, other, k in ((c.left, c.right, kind), (c.right, c.left, _SWAP[kind])): + if isinstance(local, ir.Local): + iv = self.interval(other) + if iv is None: + bottom = True + else: + _narrow(found, local.name, _bound(k, iv)) + out = dict(base) + for name, iv in found.items(): + _narrow(out, name, iv) + return out, bottom + + def recordable(self, e: ir.BinOp) -> bool: + """`proven` exists for ADD/SUB/MUL, and for FLOORDIV/MOD by a Const power of two >= 1 of a + value known to be >= 0 (the back end then emits a shift / mask); the verifier re-proves + both and cannot re-prove anything else.""" + if e.op in (ir.BinOpKind.ADD, ir.BinOpKind.SUB, ir.BinOpKind.MUL): + return True + if e.op in (ir.BinOpKind.FLOORDIV, ir.BinOpKind.MOD): + d = e.right.value if isinstance(e.right, ir.Const) else None + a = self.interval(e.left) + return (type(d) is int and d >= 1 and d & (d - 1) == 0 + and a is not None and a[0] >= 0) + return False def proved(self, e: ir.BinOp | ir.UnaryOp) -> bool: """The i64 operation provably does not deopt (no overflow, exact division).""" @@ -1302,17 +1970,81 @@ def _result_interval(self, e: ir.BinOp | ir.UnaryOp) -> Interval: if e.op == ir.BinOpKind.MUL: products = [x * y for x in a for y in b] return (min(products), max(products)) - if e.op == ir.BinOpKind.FLOORDIV: - m = max(abs(a[0]), abs(a[1])) - return (max(-m, I64_MIN), min(m, I64_MAX)) - if e.op == ir.BinOpKind.MOD: - d = max(abs(b[0]), abs(b[1]), 1) - return (-(d - 1), d - 1) return TOP # --- helpers ------------------------------------------------------------------------------------- +_NEGATE = {ir.CompareKind.EQ: ir.CompareKind.NE, ir.CompareKind.NE: ir.CompareKind.EQ, + ir.CompareKind.LT: ir.CompareKind.GE, ir.CompareKind.GE: ir.CompareKind.LT, + ir.CompareKind.LE: ir.CompareKind.GT, ir.CompareKind.GT: ir.CompareKind.LE} +_SWAP = {ir.CompareKind.EQ: ir.CompareKind.EQ, ir.CompareKind.NE: ir.CompareKind.NE, + ir.CompareKind.LT: ir.CompareKind.GT, ir.CompareKind.GT: ir.CompareKind.LT, + ir.CompareKind.LE: ir.CompareKind.GE, ir.CompareKind.GE: ir.CompareKind.LE} + + +def _clamp(iv: tuple[int, int]) -> tuple[int, int]: + return (max(iv[0], I64_MIN), min(iv[1], I64_MAX)) + + +def _meet(a: tuple[int, int], b: tuple[int, int] | None) -> tuple[int, int]: + """`a` narrowed by `b`; an empty meet (unreachable code) leaves `a`, which is still sound.""" + if b is None: + return a + lo, hi = max(a[0], b[0]), min(a[1], b[1]) + return (lo, hi) if lo <= hi else a + + +def _narrow(facts: dict[str, tuple[int, int]], name: str, iv: tuple[int, int] | None) -> None: + if iv is not None: + cur = facts.get(name) + facts[name] = iv if cur is None else _meet(cur, iv) + + +def _bound(kind: ir.CompareKind, other: tuple[int, int]) -> tuple[int, int] | None: + """The interval of `x` when `x e` is true and `e` lies in `other` (verify._bound).""" + lo, hi = other + K = ir.CompareKind + out = {K.LT: (I64_MIN, hi - 1), K.LE: (I64_MIN, hi), K.GT: (lo + 1, I64_MAX), + K.GE: (lo, I64_MAX), K.EQ: (lo, hi)}.get(kind) + return out if out is not None and out[0] <= out[1] else None + + +def _div_interval(op: ir.BinOpKind, a: tuple[int, int], b: tuple[int, int]) -> tuple[int, int]: + """Result interval of I64 FLOORDIV / MOD (verify._div_interval).""" + if op == ir.BinOpKind.MOD: + if b[0] > 0: + return (0, b[1] - 1) + if b[1] < 0: + return (b[0] + 1, 0) + d = max(abs(b[0]), abs(b[1]), 1) + return (-(d - 1), d - 1) + if b[0] > 0 or b[1] < 0: + corners = [x // y for x in a for y in b] + return (min(corners), max(corners)) + m = max(abs(a[0]), abs(a[1])) + return (-m, m) + + +def _stored_names(stmts: Iterable[ast.stmt]) -> set[str]: + """Every name stored (assigned, augmented, loop target) anywhere in `stmts`.""" + return {n.id for st in stmts for n in ast.walk(st) + if isinstance(n, ast.Name) and isinstance(n.ctx, ast.Store)} + + +def _diverges(body: tuple[ir.Stmt, ...]) -> bool: + """Control never reaches the end of `body`: it ends in a return, break or continue, or in an + `if` whose branches both do (verify.py reads these as unreachable states too).""" + if not body: + return False + last = body[-1] + if isinstance(last, (ir.Return, ir.Break, ir.Continue)): + return True + if isinstance(last, ir.If): + return _diverges(last.then) and _diverges(last.orelse) + return False + + def _walk(x): if isinstance(x, (tuple, list)): for y in x: @@ -1324,10 +2056,11 @@ def _walk(x): def _closed(fn: ir.Function, env: dict[str, ir.Function], seen: frozenset[str] = frozenset()) -> bool: - """`fn` runs no user code: no GetAttr, CallObject, Truth, CompareObj, ObjToFloat or OBJ - BinOp, and calls only closed functions.""" + """`fn` runs no user code: no GetAttr, CallObject, Truth, CompareObj, ObjToFloat, GetItem, + Contains, Format, StoreItem, ForObj or OBJ BinOp, and calls only closed functions.""" for n in _walk(fn.body): - if isinstance(n, (ir.GetAttr, ir.CallObject, ir.Truth, ir.CompareObj, ir.ObjToFloat)): + if isinstance(n, (ir.GetAttr, ir.CallObject, ir.Truth, ir.CompareObj, ir.ObjToFloat, + ir.GetItem, ir.Contains, ir.Format, ir.StoreItem, ir.ForObj)): return False if isinstance(n, ir.BinOp) and n.type == Type.OBJ: return False diff --git a/python-multiplatform-ksp/src/main/python/typedpython/importgate.py b/python-multiplatform-ksp/src/main/python/typedpython/importgate.py new file mode 100644 index 000000000..b9dcc65a8 --- /dev/null +++ b/python-multiplatform-ksp/src/main/python/typedpython/importgate.py @@ -0,0 +1,96 @@ +"""Development-mode import gate (SPEC N-7b, #183). + +`install(source_roots)` puts a meta path finder first on `sys.meta_path`. When a module whose source +file lies under one of the roots is imported, the finder runs the N-7 gate (`gate.check`, the checker +behind `python -m typedpython check`) on that file before the import machinery executes it. A type +error raises `ImportGateError`, an `ImportError` carrying the diagnostics, and none of the module's +code runs. Modules outside the roots (the stdlib, dependencies) are never checked. + +Results are cached in memory by the SHA-256 of the file content, so an unchanged module is not +checked again (a failing result is cached too). Release builds never call `install`. +""" +from __future__ import annotations + +import hashlib +import os +import sys +from collections.abc import Sequence +from importlib import machinery +from importlib.abc import MetaPathFinder +from pathlib import Path +from types import ModuleType + +from typedpython import gate +from typedpython.diagnostic import Diagnostic + +ENV_VAR = "TYPEDPYTHON_IMPORT_GATE" # source roots separated by os.pathsep + + +class ImportGateError(ImportError): + """A project module failed the type gate; `diagnostics` holds the gate's errors.""" + + def __init__(self, path: str, diagnostics: Sequence[Diagnostic], name: str | None = None) -> None: + self.diagnostics = list(diagnostics) + lines = [f"{d.path}:{d.line}:{d.column}: {d.message} [{d.rule}]" for d in self.diagnostics] + super().__init__( + f"type gate rejected {path}; the module was not run:\n" + "\n".join(lines), + name=name, path=path, + ) + + +def _run_gate(paths: Sequence[Path], search_paths: Sequence[Path]) -> list[Diagnostic]: + """The checker seam: tests count its invocations.""" + return gate.check(paths, mode="checked", search_paths=search_paths) + + +class ImportGateFinder(MetaPathFinder): + def __init__(self, source_roots: Sequence[Path]) -> None: + self.roots = [Path(r).resolve() for r in source_roots] + self._cache: dict[str, tuple[str, list[Diagnostic]]] = {} + + def find_spec( + self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = None, + ) -> machinery.ModuleSpec | None: + spec = machinery.PathFinder.find_spec(fullname, path) + origin = spec.origin if spec is not None else None + if spec is None or origin is None or not origin.endswith(".py"): + return None # not project source: let the normal finders handle it + file = Path(origin).resolve() + if not any(file.is_relative_to(root) for root in self.roots): + return None + errors = self._errors(file) + if errors: + raise ImportGateError(str(file), errors, name=fullname) + return spec + + def _errors(self, file: Path) -> list[Diagnostic]: + digest = hashlib.sha256(file.read_bytes()).hexdigest() + key = str(file) + cached = self._cache.get(key) + if cached is not None and cached[0] == digest: + return cached[1] + errors = [d for d in _run_gate([file], self.roots) if d.severity == "error"] + self._cache[key] = (digest, errors) + return errors + + +def install(source_roots: Sequence[str | Path]) -> ImportGateFinder: + """Install the gate for modules under `source_roots`, replacing an installed one.""" + uninstall() + finder = ImportGateFinder([Path(r) for r in source_roots]) + sys.meta_path.insert(0, finder) + return finder + + +def uninstall() -> None: + """Remove the gate; imports are then ungated.""" + sys.meta_path[:] = [f for f in sys.meta_path if not isinstance(f, ImportGateFinder)] + + +def install_from_env() -> bool: + """Development-mode switch: install when `TYPEDPYTHON_IMPORT_GATE` lists source roots.""" + roots = [r for r in os.environ.get(ENV_VAR, "").split(os.pathsep) if r] + if not roots: + return False + install(roots) + return True diff --git a/python-multiplatform-ksp/src/main/python/typedpython/incremental.py b/python-multiplatform-ksp/src/main/python/typedpython/incremental.py index fad342e05..30a1e8344 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/incremental.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/incremental.py @@ -7,8 +7,11 @@ The front end, the C back end and the C compiler are passed in as callables, so this file depends on none of them (only on `ir`, the contract). +The module's relative path is in the key because the C embeds it (#112): the same content moved to +another path must not reuse an extension carrying the old path. + Key encoding (v1): sha256 of the compact, key-sorted JSON of - {"v": 1, "source": sha256-hex(source bytes), + {"v": 1, "path": , "source": sha256-hex(source bytes), "deps": [[, ], ...] sorted, "compiler_version": str, "platform": str, "flags": [str, ...] in the given order} Interface hash (v1): sha256 of the compact, key-sorted JSON of @@ -49,7 +52,7 @@ def _canon(obj) -> bytes: def interface_hash(module: ir.Module) -> str: - """sha256 over the compiled functions' signatures and the skipped names — nothing else.""" + """sha256 over the compiled functions' signatures and the skipped names, nothing else.""" functions = [] for f in sorted(module.functions, key=lambda f: f.name): params = [[p.name, p.type.value, "array" if isinstance(p, ir.ArrayParam) else "scalar"] @@ -267,7 +270,7 @@ def build_project(self, sources: list[Path], project_root: Path, *, dep_hashes[rel(d)] = "src:" + _sha(d.read_bytes()) comps = {"source": source_hash, "deps": dep_hashes, "compiler_version": compiler_version, "platform": platform, "flags": list(flags)} - key = _sha(_canon({"v": _V, "source": source_hash, "deps": sorted(dep_hashes.items()), + key = _sha(_canon({"v": _V, "path": rel(p), "source": source_hash, "deps": sorted(dep_hashes.items()), "compiler_version": compiler_version, "platform": platform, "flags": list(flags)})) name = _module_name(p, root) diff --git a/python-multiplatform-ksp/src/main/python/typedpython/ir.py b/python-multiplatform-ksp/src/main/python/typedpython/ir.py index 7d0b891fa..6d1c613ae 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/ir.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/ir.py @@ -23,7 +23,7 @@ Safety (`verify.py`, maintainer decision 2026-10-03: "IR 단계에서 안전성을 증명하도록 해"). Before any C is generated, the verifier proves on the IR that the C will be memory-safe and keep the rules -above; **a function it cannot prove is not compiled** — it stays interpreted and the reason is +above; **a function it cannot prove is not compiled**, it stays interpreted and the reason is reported as a diagnostic. What it proves: - definite assignment: no local is read before it is assigned on every path; @@ -32,7 +32,8 @@ - arrays: every `Index`/`StoreIndex` is either proved in bounds (and marked `proven`) or keeps its runtime check; array params are used only by `Index`/`StoreIndex`/`Len`; - ownership: OBJ values are created only by nodes that return a new reference (Box, Global, - GetAttr, CallObject, OBJ BinOp) and the back end releases them by one rule (every OBJ local owns its reference; assigning + GetAttr, CallObject, OBJ BinOp, GetItem, Format, BuildString, NewList, NewDict, a str Const) and + the back end releases them by one rule (every OBJ local owns its reference; assigning releases the old one; every exit releases all); the IR has no node that frees, aliases a borrowed reference, or reads a possibly-NULL object, so use-after-free and NULL dereference have no IR form. @@ -81,9 +82,11 @@ class Expr: @dataclass(frozen=True) class Const(Expr): - """A literal. `value` is a Python int/float/bool/None matching `type`.""" + """A literal. `value` is a Python int/float/bool/None matching `type`. An OBJ constant is None + or a `str` (a new reference to the module-lifetime str the back end holds for it; the text has + no NUL and no lone surrogate, so it is exactly the UTF-8 the C source carries).""" - value: int | float | bool | None + value: int | float | bool | str | None @dataclass(frozen=True) @@ -108,33 +111,41 @@ class BinOp(Expr): inserts `ToFloat` for mixed int/float, as CPython converts the int). I64 (result I64, except TRUEDIV → F64): - ADD/SUB/MUL — checked; overflow → deopt. - FLOORDIV/MOD — Python floor semantics (result sign follows the divisor for MOD; the runtime + ADD/SUB/MUL, checked; overflow → deopt. + FLOORDIV/MOD, Python floor semantics (result sign follows the divisor for MOD; the runtime handles -2**63 % -1 == 0 without C's undefined behaviour); divisor 0 → ZeroDivisionError with the running interpreter's message (3.13: "integer division or - modulo by zero" / "integer modulo by zero"; 3.14: "division by zero" for every case — the + modulo by zero" / "integer modulo by zero"; 3.14: "division by zero" for every case, the runtime selects by version and its differential tests pin both); -2**63 // -1 overflows → deopt. Every ZeroDivisionError/math-domain message below follows the same rule. - TRUEDIV — CPython rounds int/int correctly; when both |operands| <= 2**53 the C double + TRUEDIV, CPython rounds int/int correctly; when both |operands| <= 2**53 the C double division is exact-rounded too, otherwise → deopt. Divisor 0 → ZeroDivisionError("division by zero"). F64: - ADD/SUB/MUL — IEEE, as CPython. - TRUEDIV — divisor 0.0 (either sign) → ZeroDivisionError("float division by zero"). - FLOORDIV/MOD — CPython's float floor division and modulo (sign of divisor, `-0.0` cases), + ADD/SUB/MUL, IEEE, as CPython. + TRUEDIV, divisor 0.0 (either sign) → ZeroDivisionError("float division by zero"). + FLOORDIV/MOD, CPython's float floor division and modulo (sign of divisor, `-0.0` cases), divisor 0.0 → ZeroDivisionError("float floor division by zero") / ("float modulo by zero"). - OBJ: CPython's generic operation on objects (PyNumber_*), never deopts. + OBJ: CPython's generic operation on objects (PyNumber_*), never deopts. `inplace` (OBJ only) is + the augmented form `a op= b` (PyNumber_InPlace*, which a list's `+=` and a user `__iadd__` + need); an exact `str + str` is PyUnicode_Concat, the function PyNumber_Add ends in for it. """ op: BinOpKind left: Expr right: Expr - # I64 ADD/SUB/MUL only: set by the front end when an interval proof shows the result cannot - # leave the i64 range; the verifier re-proves it (never trusts it) and a proven op cannot - # deopt, so it is allowed in an impure function. The back end still uses the checked helper - # and treats an overflow there as an internal error (SystemError), never as a deopt. + # I64 only, set by the front end when an interval proof shows the op cannot deopt; the + # verifier re-proves it (never trusts it) and a proven op cannot deopt, so it is allowed in an + # impure function. + # ADD/SUB/MUL: the result cannot leave the i64 range. The back end does plain arithmetic, with + # no check (#148); built with -DTP_CHECK_PROVEN it uses the checked helper and treats an + # overflow as an internal error (SystemError), never as a deopt. + # FLOORDIV/MOD: the divisor is a Const power of two >= 1 and the dividend lies in + # [0, 2**63-1] (Python's floor semantics and C's agree there); the back end emits `>>` / `&`. + # Any other op, or an op the verifier cannot prove, is rejected (verify/proven-overflow). proven: bool = False + inplace: bool = False class UnaryOpKind(Enum): @@ -164,7 +175,7 @@ class CompareKind(Enum): @dataclass(frozen=True) class Compare(Expr): """Result BOOL. Chains (`a < b < c`) are lowered by the front end into `And` with each middle - operand evaluated once (via a temporary). Mixed I64/F64 compares **exactly**, as CPython does — + operand evaluated once (via a temporary). Mixed I64/F64 compares **exactly**, as CPython does, not by converting the int to double (2**53+1 != float(2**53)). NaN compares as in IEEE.""" op: CompareKind @@ -242,13 +253,19 @@ class Call(Expr): redone); otherwise the front end calls it as an object (`CallObject(Global(name), ...)`). A call cycle (direct or mutual recursion) is legal: every compiled function counts its frame at entry (runtime `tp_enter_call`), so a recursion that CPython stops with RecursionError is stopped - with the same error, and the C stack cannot overflow.""" + with the same error, and the C stack cannot overflow. + A callee with a `Param.cls` parameter can deopt at entry whenever an argument is not proved to be + exactly that class, so such a call counts as a node that can deopt: the caller is pure (and + `may_deopt`), or the call is `redo`. A pure callee that uses a compiled class can also return the + deopt signal with `may_deopt` False, when the class changed after init (N-11); an impure caller + then redoes that callee interpreted, for OBJ and I64 results as well, such a callee is pure and + its I64 result is interval-proved, so the redone value is the same and fits.""" function: str args: tuple[Expr, ...] # Set when an impure caller calls a may_deopt (therefore pure) callee: on a deopt inside the - # callee, only the callee is redone by its interpreted function — unobservable, because the - # callee is pure — and the caller continues. Allowed only for callees returning F64, BOOL or + # callee, only the callee is redone by its interpreted function, unobservable, because the + # callee is pure, and the caller continues. Allowed only for callees returning F64, BOOL or # NONE: an F64-typed IR expression is a Python float in CPython too (ints reach floats only # through explicit ToFloat or float arithmetic), so the redone result is exactly a float; an # I64 result could be a big int and is not allowed here. @@ -268,7 +285,7 @@ class Global(Expr): @dataclass(frozen=True) class GetAttr(Expr): - """`obj.name` on an OBJ (PyObject_GetAttr — descriptors, properties and binder proxies' attribute + """`obj.name` on an OBJ (PyObject_GetAttr, descriptors, properties and binder proxies' attribute hooks run exactly as in CPython). Type OBJ. Makes the function impure: a property may have effects, so the call can never be redone.""" @@ -278,7 +295,7 @@ class GetAttr(Expr): @dataclass(frozen=True) class CallObject(Expr): - """Calling an OBJ callable — a binder Kotlin function or proxy method, any Python callable — + """Calling an OBJ callable, a binder Kotlin function or proxy method, any Python callable, with positional `args` and keyword arguments `kwnames` (the last len(kwnames) args are the keyword values, as in vectorcall). Arguments are OBJ (scalars arrive through Box). Type OBJ. Makes the function impure. This is the general path that keeps Kotlin interop available to @@ -302,7 +319,7 @@ class CompareObj(Expr): """A comparison of two OBJ operands (a scalar side arrives through `Box`) **directly as a condition**: an If/While cond, or an And/Or operand when that And/Or is itself the condition. Semantics of `if a == b:` in CPython: the rich comparison runs, then the truth value of its - result — **without** PyObject_RichCompareBool's identity shortcut (a NaN float compared with + result, **without** PyObject_RichCompareBool's identity shortcut (a NaN float compared with itself is False in CPython's `if x == x`). Type BOOL. Anywhere the comparison's own result object would be kept (`x = a < b`, `r = c and a < b`, `not (a < b)`) it is not allowed, because that object need not be a bool.""" @@ -342,11 +359,130 @@ class CopyArray(Expr): @dataclass(frozen=True) class Tuple(Expr): - """`(a, b, ...)` of scalars/OBJs — a new tuple (type OBJ). Scalar elements arrive through Box.""" + """`(a, b, ...)` of scalars/OBJs, a new tuple (type OBJ). Scalar elements arrive through Box.""" elements: tuple[Expr, ...] +@dataclass(frozen=True) +class GetItem(Expr): + """`obj[index]` on OBJ operands (obj first, then index): PyObject_GetItem, the call the + interpreter's BINARY_OP [] makes, so `__getitem__`, `__missing__`, `__hash__`/`__eq__` of the + key, `__index__` and every error (KeyError(key), IndexError, TypeError) are CPython's. For an + object that is exactly a `dict` it is PyDict_GetItemRef and, when absent, KeyError(key) built as + dict's own subscript does (runtime `tp_getitem`; the guard is the exact type). Type OBJ. Makes the + function impure and not closed.""" + + obj: Expr + index: Expr + + +@dataclass(frozen=True) +class Contains(Expr): + """`left in right` / `left not in right` (negate) on OBJ operands (left first, then right): + PySequence_Contains(right, left), the call CONTAINS_OP makes, so `__contains__`, iteration and + `__eq__` (with RichCompareBool's identity shortcut) run as in CPython. Type BOOL, always a bool + (so unlike CompareObj it is allowed anywhere). Makes the function impure and not closed.""" + + left: Expr + right: Expr + negate: bool = False + + +@dataclass(frozen=True) +class NewList(Expr): + """`[]`: PyList_New(0). Type OBJ. A fresh object is not an effect.""" + + +@dataclass(frozen=True) +class NewDict(Expr): + """`{}`: PyDict_New(). Type OBJ. A fresh object is not an effect.""" + + +@dataclass(frozen=True) +class Format(Expr): + """One `{value!c:spec}` field of an f-string, exactly as FORMAT_VALUE / CONVERT_VALUE + + FORMAT_SIMPLE / FORMAT_WITH_SPEC do: apply the conversion (`conversion`: -1 none, 115 `!s` + PyObject_Str, 114 `!r` PyObject_Repr, 97 `!a` PyObject_ASCII), then with `spec` (a constant str) + PyObject_Format(value, spec), without it the value itself when it is exactly a str, else + PyObject_Format(value, NULL). Type OBJ (a str unless a user `__format__` returned something + else, which PyObject_Format rejects with CPython's TypeError). Impure and not closed.""" + + operand: Expr + spec: str | None + conversion: int + + +@dataclass(frozen=True) +class BuildString(Expr): + """The f-string itself (BUILD_STRING): the str that joins `elements` (OBJ strs: Const strs and + Format results), evaluated in order, each Format running before the next element is evaluated. + Type OBJ. Runs no user code.""" + + elements: tuple[Expr, ...] + + +@dataclass(frozen=True) +class Is(Expr): + """`a is b` / `a is not b` (negate) on OBJ operands: identity, no user code. Type BOOL. With + `Const(None, OBJ)` this is how `x is None` is written.""" + + left: Expr + right: Expr + negate: bool = False + + +@dataclass(frozen=True) +class IsExact(Expr): + """`type(obj) is C` for a compiled fixed-layout class C (SPEC N-11): no user code. Type BOOL. + A true IsExact dominating a FieldGet/FieldSet on the same local is what the verifier accepts as + the exact-type proof for that access. `C` is the module global as CPython reads it: when the + global no longer is the class captured at init, the back end compares with the current binding + (impure) or deopts (pure).""" + + obj: Expr + cls: str + + +@dataclass(frozen=True) +class CheckExact(Expr): + """`obj` itself (type OBJ), after checking `type(obj) is C`; anything else → deopt. Because it + can deopt it is legal only in a pure function, the same rule as Unbox. It is the proof the + verifier accepts for a field access the front end cannot guard with a dominating IsExact.""" + + obj: Expr + cls: str + + +@dataclass(frozen=True) +class FieldGet(Expr): + """`obj.field` of a compiled fixed-layout class C whose exact type is proved (a dominating true + IsExact on the same local, a CheckExact, a New, or a parameter guarded `type(x) is C`). Reads the + slot through the member descriptor captured at module init: a new reference, or CPython's own + AttributeError for an unset slot. Runs no user code, so it is not an effect. Type OBJ. + The class may change after init (a property replacing the field, `obj.__class__ = Other`): the + back end re-checks `type(obj) is C` and C's version tag at the access, and otherwise does what + CPython does, `getattr(obj, field)` in an impure function, a deopt in a pure one (where running + user code could be repeated by a later deopt's redo). FieldSet likewise (setattr).""" + + obj: Expr + cls: str + field: str + + +@dataclass(frozen=True) +class New(Expr): + """`C(args)` for a compiled class whose `__init__` only assigns each slot once from its parameters + (ClassDecl.trivial_init): allocate with C's tp_alloc and fill the slots in __init__'s order, + the observable result of running that __init__. A fresh object is not an effect. Type OBJ; args + are OBJ (scalars through Box). `C` is read as a module global before the args, as CPython's + LOAD_GLOBAL is: if it is no longer the captured class, or the class changed (e.g. __init__), the + back end calls what the global held (impure) or deopts (pure).""" + + cls: str + args: tuple[Expr, ...] + + @dataclass(frozen=True) class Len(Expr): """`len(array)` of an array parameter. Type I64.""" @@ -391,6 +527,49 @@ class StoreIndex(Stmt): proven: bool = False # as in Index +@dataclass(frozen=True) +class FieldSet(Stmt): + """`obj.field = value` on a proved-exact compiled class (as FieldGet). An effect, unless `obj` is + a New made in this same function and not yet escaped (not assumed in M2b: always an effect).""" + + obj: Expr + cls: str + field: str + value: Expr + + +@dataclass(frozen=True) +class StoreItem(Stmt): + """`obj[index] = value` on OBJ operands. CPython evaluates the value first, then the container, + then the index: the back end evaluates them in that order (the three operands are expressions + that may be effects). PyObject_SetItem, the call STORE_SUBSCR makes (for an object that is exactly + a `dict`, PyDict_SetItem, which is what dict's `mp_ass_subscript` calls). An effect, and not + closed: `__setitem__`, `__hash__`/`__eq__` of the key and dropping the old value run user code.""" + + obj: Expr + index: Expr + value: Expr + + +@dataclass(frozen=True) +class ForObj(Stmt): + """`for t in iter:` or `for t0, t1, ... in iter:` over any object: `iter` (OBJ) is evaluated once, + GET_ITER is PyObject_GetIter, each step is PyIter_Next (the call FOR_ITER ends in: StopIteration + is the end, any other exception propagates); the item is stored into the OBJ locals `targets` + left to right. With `unpack` the item is unpacked into the targets as UNPACK_SEQUENCE does (an + exact tuple of the right length directly, anything else through its iterator, with CPython's + `cannot unpack non-iterable T object`, `not enough values to unpack (expected n, got k)` and + `too many values to unpack (expected n[, got k])` errors); without it there is exactly one target + and it receives the item. The targets keep their last value after the loop; a loop that ran zero + times leaves them as they were. `break`/`continue` and every exit release the iterator once. + The effect of GetIter/Next is user code, so the function is impure and not closed.""" + + targets: tuple[str, ...] + iter: Expr + body: tuple[Stmt, ...] + unpack: bool = False + + @dataclass(frozen=True) class ExprStmt(Stmt): value: Expr @@ -414,7 +593,7 @@ class ForRange(Stmt): """`for var in range(start, stop, step)`: `step` is a non-zero I64 Const; `start`/`stop` are evaluated once before the loop, as range() does. `var` is I64 and is not assigned in the body (the front end checks), so it never overflows: it stays between start and stop. After the loop - `var` keeps its last value, and if the loop ran zero times it keeps its previous binding — the + `var` keeps its last value, and if the loop ran zero times it keeps its previous binding, the front end rejects reading it after the loop unless it was definitely assigned before.""" var: str @@ -445,6 +624,10 @@ class Continue(Stmt): class Param: name: str type: Type + # For an OBJ parameter annotated with a compiled class C: the entry guard checks + # `type(x) is C` (or `x is None` too when `optional`), deopting before any effect otherwise. + cls: str | None = None + optional: bool = False @dataclass(frozen=True) @@ -454,9 +637,9 @@ class ArrayParam: At entry (guards, before any effect): the argument is exactly a `list`, every element is exactly `float` (F64_ARRAY) or exactly `int` within i64 (I64_ARRAY), and no two array parameters are the same list object (aliasing would make separate copies diverge). Then the elements are copied - into a C array. The function body only indexes, stores into and takes `len` of it (no calls that - could observe the list, no appends: the front end checks), so the list and the array cannot - diverge observably. On **every** exit — return, exception, or deopt — dirty elements are + into a C array. The function body only indexes, stores into and takes `len` of it, no appends, and + runs no user code at all: the verifier rejects a function holding an array that is not closed + (`verify/array-open`, #203), so the list and the array cannot diverge observably. On **every** exit, return, exception, or deopt, dirty elements are written back as new float/int objects before control leaves the function, so CPython's partial-update-then-raise behaviour is preserved, and a deopt redo starts from the same list state the interpreted function would see... which is only correct because a function with array @@ -466,6 +649,18 @@ class ArrayParam: name: str type: Type # F64_ARRAY or I64_ARRAY stored: bool # whether the body stores into it (write-back needed) + # Buffer array (#149): None for a `list`. "array" (an exact `array.array`) means + # the argument is NOT copied: the entry guard takes a Py_buffer export + # (exact type, 1-D, C-contiguous, aligned 8-byte items, format `d` for F64_ARRAY and `q` or `l` + # for I64_ARRAY, writable when `stored`), holds it until the function returns on every exit, and + # every Index/StoreIndex is one relaxed atomic 8-byte load/store on the buffer memory, no + # copy-in, no write-back, no critical section, no aliasing guard (two buffers may share memory + # and reads then see writes in program order, as in the interpreter). `len` is the export's + # shape[0], fixed for the call because the export forbids resizing. Closedness (#203) applies + # unchanged. numpy.ndarray is not a buffer kind (its element arithmetic is numpy's: np.float64 + # results, RuntimeWarnings, no ZeroDivisionError); an ndarray parameter is an ordinary object. + # Every other rule above for lists (stored, array-open, Index/StoreIndex/Len only) holds too. + buffer: str | None = None @dataclass(frozen=True) @@ -476,24 +671,24 @@ class Function: interpreted; `frontend` records why). Local arrays (fannkuch, M2): a local of an array type holds a native array the function owns, - created only by NewArray/CopyArray. It never escapes — used only by Index/StoreIndex/Len and - as a CopyArray source; never returned, passed, boxed or captured — so stores into it are not + created only by NewArray/CopyArray. It never escapes, used only by Index/StoreIndex/Len and + as a CopyArray source; never returned, passed, boxed or captured, so stores into it are not effects (a function whose only stores are into local arrays can be pure), and no list object ever exists for it. Reassigning the local frees the old array; every exit frees all of them. Its length never changes after creation, so a bounds proof against Len(local) holds. - `pure`: no effect outside its own locals — no stores into anything but locals, no CallObject, - no GetAttr (a property can have effects), no Truth/CompareObj/ObjToFloat/OBJ BinOp (they run - user methods), only `Call`s to pure functions. + `pure`: no effect outside its own locals, no stores into anything but locals, no CallObject, + no GetAttr (a property can have effects), no Truth/CompareObj/ObjToFloat/OBJ BinOp/GetItem/ + Contains/Format/StoreItem/ForObj (they run user methods), only `Call`s to pure functions. Kotlin interop (maintainer: compiled code must still talk to Kotlin freely): a function that - calls into the binder is impure, so its int arithmetic cannot use the deopting I64 path — + calls into the binder is impure, so its int arithmetic cannot use the deopting I64 path, unbounded ints there are OBJ (CPython's own int objects, PyNumber_*), while floats (F64 never deopts), bounded loop indices, and the loops around the calls are still native. Only a pure function may contain deopting nodes after entry. Entry guards are generated by the back end from the params: exact runtime type per scalar/array - param (`type(x) is float`, not isinstance — CPython would compute with the int or the subclass + param (`type(x) is float`, not isinstance, CPython would compute with the int or the subclass it was given), i64 range for I64, the ArrayParam checks. A failing guard → deopt (always safe: nothing has happened yet). Wrong arity or keyword use → deopt too, so CPython raises its own TypeError. A function returning a value must return on every path (no falling off the end). @@ -509,15 +704,29 @@ class Function: source_line: int # Module globals of a scalar type read ONCE at entry, as part of the guards: each must be # exactly that type (else deopt, before any effect), and the body reads it as `Local(name)`. - # Legal only for a *closed* function — one that runs no user code (no GetAttr, CallObject, - # Truth, CompareObj, ObjToFloat, OBJ BinOp; Calls only to closed functions) — so nothing can + # Legal only for a *closed* function, one that runs no user code (no GetAttr, CallObject, + # Truth, CompareObj, ObjToFloat, OBJ BinOp, GetItem, Contains, Format, StoreItem, ForObj; Calls + # only to closed functions), so nothing can # rebind the global between entry and use, and reading it once equals reading it at each use. entry_globals: tuple[Param, ...] = () +@dataclass(frozen=True) +class ClassDecl: + """A compiled fixed-layout class (SPEC N-11): `__slots__` written by the user equal to its + annotated fields, base object, no metaclass, no decorator but @compiled. The module init checks + every field's class attribute is a member descriptor and captures it; if not, the class is + not compiled (functions using it deopt at entry or are refused by the verifier).""" + + name: str + fields: tuple[str, ...] + trivial_init: bool # __init__(self, *fields-in-order) doing exactly `self.f = f` once each + + @dataclass(frozen=True) class Module: name: str functions: tuple[Function, ...] # Functions the front end left interpreted, with the reason (for `demo` and the coverage table). skipped: dict[str, str] = field(default_factory=dict) + classes: tuple[ClassDecl, ...] = () diff --git a/python-multiplatform-ksp/src/main/python/typedpython/pipeline.py b/python-multiplatform-ksp/src/main/python/typedpython/pipeline.py index db3e2ffe1..3f504651c 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/pipeline.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/pipeline.py @@ -4,7 +4,7 @@ 1. The gate in `compiled` mode: a module with type errors is not compiled at all. 2. `frontend.lower`: functions outside the IR are left interpreted (`skipped`, with reasons). -3. `verify.verify`: functions the verifier cannot prove are left interpreted too (SPEC N-9 — +3. `verify.verify`: functions the verifier cannot prove are left interpreted too (SPEC N-9, unsafe code is never generated silently). 4. If nothing is left to compile, no C is produced (`extension is None`): the module ships as ordinary Python. @@ -12,12 +12,13 @@ """ import hashlib import json +import re import sysconfig from dataclasses import dataclass, field from pathlib import Path from types import ModuleType -from typedpython import cbuild, cgen, frontend, gate, incremental, ir, verify +from typedpython import cbuild, cgen, cost, frontend, gate, incremental, ir, verify class CompileError(RuntimeError): @@ -33,6 +34,11 @@ class Result: diagnostics: list[verify.Diagnostic] = field(default_factory=list) +def _relative(path: Path, root: Path) -> str: + """The module's path relative to the project root, posix separators (what the C embeds).""" + return Path(path).resolve().relative_to(Path(root).resolve()).as_posix() + + def _gate_or_raise(path: Path) -> None: errors = [d for d in gate.check([path], mode="compiled") if d.severity == "error"] if errors: @@ -47,17 +53,26 @@ def compile_module( python_include: Path | None = None, ext_suffix: str | None = None, runtime_dir: Path | None = None, + project_root: Path | None = None, + cost_rule: bool = True, ) -> Result: + """`project_root`: the root the embedded source path is relative to (#112); default: the source's + own directory, so the file name alone is embedded. `cost_rule=False` skips the cost policy + (`cost.py`) and compiles object-only functions too; it exists for tests of the object operations + and is never set by a user-facing path.""" path = Path(path).resolve() + root = path.parent if project_root is None else Path(project_root).resolve() _gate_or_raise(path) lowered = frontend.lower(path) proved, diagnostics = verify.verify(lowered) + if cost_rule: + proved, diagnostics = cost.apply(proved, diagnostics) # policy after proof, see cost.py skipped = dict(proved.skipped) if not proved.functions: return Result(path, proved, None, skipped, diagnostics) - c_source = cgen.generate(proved, path) + c_source = cgen.generate(proved, path, _relative(path, root)) extension = cbuild.build( c_source, path.stem, Path(out_dir), runtime_dir=runtime_dir, python_include=python_include, ext_suffix=ext_suffix, @@ -98,6 +113,36 @@ def _tree_hash(*dirs: Path) -> str: _NO_EXTENSION = ".noext" # cache artefact of a module with nothing compiled +@dataclass(frozen=True) +class SkippedItem: + """A function or class that stays interpreted, and why (pypackpack's native-level report).""" + name: str + file: str # package-relative, posix + line: int | None + message: str # without the "line N: " prefix + + +_LINE_PREFIX = re.compile(r"line (\d+): ") + + +def skipped_items(skipped: dict[str, str], diagnostics: list[verify.Diagnostic], file: str) -> list[SkippedItem]: + """Structure `ir.Module.skipped` (name -> text, which stays as it is: the incremental interface + hash reads its keys only). The front end writes `line N: reason`; the verifier writes `rule: + message` and records the line in its diagnostic for the function.""" + lines: dict[str, int] = {} + for d in diagnostics: + lines.setdefault(d.function, d.source_line) + out = [] + for name, text in sorted(skipped.items()): + m = _LINE_PREFIX.match(text) + if m: + line, message = int(m.group(1)), text[m.end():] + else: + line, message = lines.get(name), text + out.append(SkippedItem(name, file, line, message)) + return out + + @dataclass class ModuleResult: path: Path @@ -107,6 +152,8 @@ class ModuleResult: reasons: list[str] = field(default_factory=list) # why it was rebuilt (empty if reused) skipped: dict[str, str] = field(default_factory=dict) # function -> why it stays interpreted interface: str = "" + kind: str = "compiled" # "compiled" | "nothing_compiled" | "not_marked" + skipped_items: list[SkippedItem] = field(default_factory=list) # `skipped`, structured @dataclass @@ -126,19 +173,25 @@ def compile_project( python_include: Path | None = None, ext_suffix: str | None = None, runtime_dir: Path | None = None, + cost_rule: bool = True, ) -> ProjectResult: sources = [Path(s).resolve() for s in sources] cache_dir = Path(cache_dir) - verified: dict[Path, ir.Module] = {} + verified: dict[Path, list[SkippedItem]] = {} + verified_module_skipped: dict[Path, dict[str, str]] = {} + root = Path(project_root).resolve() def lower(path: Path) -> ir.Module: _gate_or_raise(path) - proved, _ = verify.verify(frontend.lower(path)) - verified[path] = proved # the VERIFIED module: its signatures are what gets compiled + proved, diagnostics = verify.verify(frontend.lower(path)) + if cost_rule: + proved, diagnostics = cost.apply(proved, diagnostics) + verified_module_skipped[path] = dict(proved.skipped) + verified[path] = skipped_items(proved.skipped, diagnostics, _relative(path, root)) # the VERIFIED module: its signatures are what gets compiled return proved def generate(module: ir.Module, path: Path) -> str: - return cgen.generate(module, path) if module.functions else "" + return cgen.generate(module, path, _relative(path, root)) if module.functions else "" def compile_c(c_source: str, name: str, out_dir: Path) -> Path: if not c_source: @@ -151,6 +204,8 @@ def compile_c(c_source: str, name: str, out_dir: Path) -> Path: version = COMPILER_VERSION if runtime_dir is not None: version += ":" + _tree_hash(Path(runtime_dir)) + if not cost_rule: + version += ":nocost" suffix = ext_suffix if ext_suffix is not None else sysconfig.get_config_var("EXT_SUFFIX") include = cbuild._python_include(python_include) flags = [*cbuild.DEFAULT_FLAGS, f"-I{include}"] @@ -166,11 +221,18 @@ def compile_c(c_source: str, name: str, out_dir: Path) -> Path: for m in report.modules: record = side / f"{m.key}.json" if m.status == "rebuilt": - record.write_text(json.dumps(dict(verified[m.path].skipped))) + record.write_text(json.dumps({ + "skipped": dict(verified_module_skipped[m.path]), + "items": [vars(i) for i in verified[m.path]]})) try: - skipped = json.loads(record.read_text()) - except (OSError, ValueError): - skipped = {} + data = json.loads(record.read_text()) + skipped = dict(data["skipped"]) + items = [SkippedItem(**i) for i in data["items"]] + except (OSError, ValueError, KeyError, TypeError): + skipped, items = {}, [] ext = None if m.artifact is None or m.artifact.suffix == _NO_EXTENSION else m.artifact - out.append(ModuleResult(m.path, m.name, ext, m.status, list(m.reasons), skipped, m.interface)) + kind = ("not_marked" if not frontend.is_opted_in(m.path) + else "compiled" if ext is not None else "nothing_compiled") + out.append(ModuleResult(m.path, m.name, ext, m.status, list(m.reasons), skipped, m.interface, + kind, items)) return ProjectResult(out) diff --git a/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md b/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md index 77bd67469..64e181e58 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md +++ b/python-multiplatform-ksp/src/main/python/typedpython/runtime/API.md @@ -1,15 +1,25 @@ -# tp_runtime.h — the C runtime API generated code may call +# tp_runtime.h, the C runtime API generated code may call Generated C (cgen.py) reaches memory and `PyObject`s **only** through these helpers (SPEC N-9). Header-only, `static inline`, C99 + CPython C API, no other dependencies. Every helper implements the CPython behaviour named in `ir.py`. Return convention unless stated: `0` ok, `1` deopt (no -Python error set, nothing to undo), `-1` Python error set (CPython's exception type and message — **of the running interpreter**: 3.14 changed several ZeroDivisionError and math-domain messages, and the runtime selects them by PY_VERSION_HEX). +Python error set, nothing to undo), `-1` Python error set (CPython's exception type and message, **of the running interpreter**: 3.14 changed several ZeroDivisionError and math-domain messages, and the runtime selects them by PY_VERSION_HEX). ## Integers (int64_t) int tp_add_i64(int64_t a, int64_t b, int64_t *out); /* 0 / 1 on overflow */ int tp_sub_i64(int64_t a, int64_t b, int64_t *out); int tp_mul_i64(int64_t a, int64_t b, int64_t *out); int tp_neg_i64(int64_t a, int64_t *out); /* 1 for INT64_MIN */ + /* Proven operations (ir.BinOp.proven, #148): the verifier showed the result fits in i64, so the + generated code calls these (function-like macros; no check, no status). With + -DTP_CHECK_PROVEN they are the checked helpers above and a wrong proof is a SystemError. A + proven // or % by a power of two is emitted inline as `>>` / `&`, not through a helper. */ + tp_proven(add|sub|mul, a, b, t); /* t = a op b, no overflow check */ + tp_proven_neg(a, t); /* t = -a */ + int64_t tp_padd_i64(int64_t a, int64_t b); /* the unchecked forms behind them */ + int64_t tp_psub_i64(int64_t a, int64_t b); + int64_t tp_pmul_i64(int64_t a, int64_t b); + int64_t tp_pneg_i64(int64_t a); int tp_floordiv_i64(int64_t a, int64_t b, int64_t *out); /* -1 ZeroDivisionError; 1 for MIN // -1 */ int tp_mod_i64(int64_t a, int64_t b, int64_t *out); /* -1 ZeroDivisionError; sign of b */ int tp_truediv_i64(int64_t a, int64_t b, double *out); /* -1 ZeroDivisionError; 1 if |a| or |b| > 2**53 */ @@ -61,18 +71,105 @@ Python error set, nothing to undo), `-1` Python error set (CPython's exception t /* aliasing guard: 1 if any two of the n objects are the same object */ int tp_any_same(PyObject *const *objs, Py_ssize_t n); + /* Free-threaded CPython (#159): the wrapper holds the lists' critical section from before the + first enter to after the last exit, on every path (ok, error, deopt). tp_cs is zero-initialised + (`tp_cs c = {0};`). b == NULL locks one list, else both (PyCriticalSection2: ordered by + address, so two threads locking (a, b) and (b, a) cannot deadlock). More than two list + parameters are not compiled (frontend.Skip, verify/structure, CGenError): CPython has no + three-object section and a nested third would suspend the others. With a GIL both are no-ops. */ + typedef struct { int n; /* + a PyCriticalSection2 on free-threaded builds */ } tp_cs; + void tp_cs_begin(tp_cs *c, PyObject *a, PyObject *b); + void tp_cs_end(tp_cs *c); /* idempotent: a no-op when nothing is held */ +`tp_*_array_enter` / `_exit` read and write the list through borrowed references (`PyList_GET_ITEM`, +`PyList_SetItem`) and REQUIRE the caller to hold that critical section. Between enter and exit no +other thread can change the list, so the whole call is one atomic step as far as lists go (an +interleaving CPython can produce: each list operation takes the same lock). A long compiled array +loop does not poll (below), so it delays other threads' stop-the-world requests (the free-threaded +GC) until it returns: a liveness delay, results unaffected. Polls inside array-holding functions +need the re-validation decided in issue #152 and are not added. + +## Buffer arrays (issue #149; `ArrayParam.buffer` = "array") +An exact `array.array` argument (`array.array[float]` / `array.array[int]`; the names must resolve +through the module's own top-level imports) is not copied. + typedef struct { Py_buffer view; Py_ssize_t len; void *data; int held; } tp_buf; + int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2, int writable, tp_buf *b); + void tp_buf_exit(tp_buf *b); /* PyBuffer_Release if held; zeroes b; idempotent */ + int tp_buf_slot(const tp_buf *b, int64_t i, int store, Py_ssize_t *slot); /* 0 / -1, array.array's IndexError text */ + double tp_buf_ld_f64(const tp_buf *b, Py_ssize_t i); void tp_buf_st_f64(const tp_buf *b, Py_ssize_t i, double v); + int64_t tp_buf_ld_i64(const tp_buf *b, Py_ssize_t i); void tp_buf_st_i64(const tp_buf *b, Py_ssize_t i, int64_t v); + int tp_import_attr(const char *mod, const char *attr, PyObject **out); /* exec slot: new ref or NULL (cleared); -1 MemoryError */ +`enter` returns 0 (held), 1 (deopt: b zeroed, nothing held, no exception) or -1 (MemoryError). It +deopts unless: `exact` is non-NULL and `Py_IS_TYPE(obj, exact)` (a subclass may override +`__getitem__`); `PyObject_GetBuffer(C_CONTIGUOUS | FORMAT [| WRITABLE when the function stores])` +succeeds; ndim 1, itemsize 8, no suboffsets, `shape[0] * 8 == len`; the format string is exactly +`d` (floats) or `q` / `l` (ints; a byte-order prefix, `Q`, `i` ... fail); the data is 8-byte aligned. +The wrapper calls `exit` on every path (ok, error, deopt), so the export lives exactly as long as the +compiled call. Element access is one relaxed atomic 8-byte load or store (`__atomic_load_n` / +`__atomic_store_n` on `uint64_t`, bit-cast with `memcpy`); `-DTP_BUFFER_PLAIN` replaces them with +plain loads and stores, for measuring what the atomics cost only. `len` is `shape[0]`, fixed for the +call by the export. + +Rules that go with it (verifier / front end): +* Closed (#203) like a list: a function with a buffer runs no user code (`verify/array-open`). +* No limit on the number of buffer parameters (no critical section); lists keep the limit of two. + A buffer and a list cannot alias (different types). Two buffers may share memory (the same array + passed twice): per-element access makes reads see earlier writes in program order, as in + the interpreter, which a copy-in could not. +* `y = x[:]` of a buffer is not a native copy: the function stays interpreted. +* numpy: not supported yet (#220). An `NDArray[...]` / `np.ndarray` parameter is an ordinary object + parameter. Reasons: (1) arithmetic on numpy elements produces `np.float64`, `np.int64` and + `np.bool` in the interpreter (a loop `s += v` returns `np.float64`), compiled code returns `float`; + (2) numpy scalar operations emit `RuntimeWarning`s ("overflow encountered in scalar multiply", + "invalid value encountered in scalar add") and divide by zero warns instead of raising, which + compiled double arithmetic does neither, and warnings are observable; (3) emulating this needs taint + tracking of numpy-derived values, numpy-scalar boxing and FP-exception-to-warning emulation. +* OPEN POINT (decision pending in #149), deliberately not decided here and not tested: holding the + export makes another thread's `append` / `resize` / `extend` on the same `array.array` raise + `BufferError` for the duration of the call; what a concurrent resize should observe instead is not + settled. +* Free-threaded: no critical section; every element access is an atomic, so a concurrent writer of the + same element yields one whole value, never a torn one. A function holding a buffer does not poll the + eval breaker (same liveness note as lists, #152). + ## Opaque objects (Kotlin interop and any Python object; ir.Global/GetAttr/CallObject/Truth/CompareObj/ObjToFloat) All return new references or NULL with CPython's exception set; none deopts. PyObject *tp_global(PyObject *module_dict, PyObject *name); /* globals, then builtins; NameError("name 'x' is not defined") */ PyObject *tp_getattr(PyObject *obj, PyObject *name); /* PyObject_GetAttr */ PyObject *tp_call(PyObject *callee, PyObject *const *args, size_t nargs, PyObject *kwnames); /* PyObject_Vectorcall: nargs = positional count; keyword values follow at args[nargs..] */ - /* tp_binop_obj op codes: TP_BINOP_ADD..TP_BINOP_MOD = 0..5 in ir.BinOpKind declaration order */ + /* tp_binop_obj op codes: TP_BINOP_ADD..TP_BINOP_MOD = 0..5 in ir.BinOpKind declaration order; + TP_BINOP_IADD..TP_BINOP_IMOD = 6..11 are the augmented forms (PyNumber_InPlace*, ir.BinOp.inplace) */ int tp_truth(PyObject *obj); /* 0/1, -1 error (PyObject_IsTrue) */ int tp_compare_bool(PyObject *a, PyObject *b, int op); /* 0/1, -1 error: PyObject_RichCompare then PyObject_IsTrue (no identity shortcut, as `if a == b`) */ int tp_obj_to_f64(PyObject *obj, double *out); /* 0 / -1 (PyNumber_Float, then exact double) */ PyObject *tp_binop_obj(PyObject *a, PyObject *b, int op); /* PyNumber_Add/... for ir.BinOpKind on OBJ */ void tp_release(PyObject **slot); /* Py_CLEAR: the only way generated code drops a reference */ +## Objects: subscript, in, f-strings, iteration (issue #150; ir.GetItem / StoreItem / Contains / NewList / NewDict / Format / BuildString / ForObj / a str Const) +Every helper makes the C API call the interpreter's bytecode makes, so results, exceptions (type, +message, args) and the user `__hash__` / `__eq__` / `__index__` / `__getitem__` / `__format__` / +`__iter__` calls, and their order, are CPython's. New references or NULL / -1 with the exception set; +none deopts. A fast path replaces a generic call only under an exact-type guard, and only with the C +function the generic call ends in for that exact type: the two guards are the macros +`TP_FASTPATH_GUARD_DICT(o)` (`PyDict_CheckExact`) and `TP_FASTPATH_GUARD_STR(o)` +(`PyUnicode_CheckExact`), the single mutation points the tests widen to `PyDict_Check` / +`PyUnicode_Check` and must see fail. A `dict` or `str` subclass (which can override +`__getitem__`, `__setitem__`, `__add__`, ...) always takes the generic call. + PyObject *tp_getitem(PyObject *obj, PyObject *key); /* BINARY_OP []: PyObject_GetItem; exact dict: PyDict_GetItemRef + KeyError(key) as dict_subscript raises it */ + int tp_setitem(PyObject *obj, PyObject *key, PyObject *value); /* STORE_SUBSCR: PyObject_SetItem; exact dict: PyDict_SetItem. 0 / -1 */ + int tp_contains(PyObject *container, PyObject *needle); /* CONTAINS_OP: PySequence_Contains; 1 / 0 / -1 */ + PyObject *tp_new_list(void); /* PyList_New(0) */ + PyObject *tp_new_dict(void); /* PyDict_New() */ + PyObject *tp_format(PyObject *value, PyObject *spec, int conversion); /* one f-string field: conversion -1 / 's' / 'r' / 'a' (PyObject_Str / Repr / ASCII), then spec != NULL: PyObject_Format(v, spec); else an exact str as it is, else PyObject_Format(v, NULL) */ + PyObject *tp_build_string(PyObject *const *parts, Py_ssize_t n); /* BUILD_STRING: PyUnicode_Join("", parts); parts are borrowed */ + PyObject *tp_get_iter(PyObject *obj); /* GET_ITER: PyObject_GetIter */ + PyObject *tp_iter_next(PyObject *iter); /* FOR_ITER: PyIter_Next; NULL without an error at the end */ + int tp_unpack(PyObject *v, Py_ssize_t n, PyObject **out); /* UNPACK_SEQUENCE: an exact tuple of length n directly, else through its iterator with the interpreter's "cannot unpack non-iterable T object" / "not enough values to unpack (expected n, got k)" / "too many values to unpack (expected n[, got k])"; 0 with n new references in out, or -1 with none */ +`str + str` (both exactly str) in `tp_binop_obj` is `PyUnicode_Concat`, which `PyNumber_Add` ends in for two strs. +Free-threaded: containers are read with the strong-reference API (`PyDict_GetItemRef`); an exact +tuple's items may be borrowed because a tuple is immutable; a list or any other object is read +through its iterator, so no borrowed reference into a container outlives the call that returned it. +A str constant is an interned object held in the module state (`names`, immutable after init). + ## Local arrays (ir.NewArray / CopyArray; owned by the function, no list object) Same structs as above with `list == NULL` and `dirty == NULL`. 0 ok, -1 MemoryError. int tp_i64_array_new(tp_i64_array *a, int64_t n, int64_t fill); /* n < 0 → empty */ @@ -95,5 +192,177 @@ object, so a pure compiled recursion pays O(1) per call) and compares the total `Py_GetRecursionLimit()`. Over it: `RecursionError("maximum recursion depth exceeded")`, the text CPython's own Python-frame check uses. It then calls `Py_EnterRecursiveCall(" in compiled code")`, the C-stack guard: a raised recursion limit can never let compiled C overflow the stack. Counters are -per module (the header is `static`): frames of a *different* compiled module that sit between two -frames of this one are not counted. +per module (the header is `static`) and per thread (`TP_THREAD_LOCAL`): frames of a *different* +compiled module that sit between two frames of this one are not counted, and another thread's +frames never are. + +## Fast entry (bounded functions; issue #147) + int tp_depth_room(Py_ssize_t k); /* 1 iff tp_python_depth() + count + k <= Py_GetRecursionLimit() */ + void tp_trace_fast_hit(void); /* counts a fast entry; a no-op unless -DTP_TRACE_FAST */ + void tp_depth_py_begin(int fd); /* count += fd, around code a fast tree may run (redo, poll) */ + void tp_depth_py_end(int fd); /* count -= fd; drops the cached frame at count 0 */ + void tp_depth_done(void); /* after a fast tree: drop the cached top frame when count is 0 */ +A function is *bounded* when it is in no call cycle, every function it Calls is bounded, and its body +has no node that can run Python code (CallObject, GetAttr, Truth, CompareObj, ObjToFloat, FieldGet, +FieldSet, New, any OBJ-typed expression except Box of a scalar). A `redo` Call to a bounded callee is allowed. Its k is `1 + max(k(callee))` (1 with no +callees). cgen emits, per bounded `f`, `static inline int tp_fimpl_f(...)` (the impl body without +tp_enter_call / tp_leave_call and without the entry poll, Calls to the callees' `tp_fimpl_*`) and, at +the top of `tp_impl_f` after the Param.cls guards, `if (tp_depth_room(k)) { r = tp_fimpl_f(...); +tp_depth_done(); return r; }`. No room: the counted path runs and raises RecursionError exactly where +it raised before. `tp_enter_call` compares `depth + count + 1`; the fast tree has at most k frames, so +`depth + count + k` is the same comparison at its deepest frame. Py_EnterRecursiveCall is skipped on +the fast path: the tree is bounded, so it adds at most k small C frames. A twin takes `int tp_fd` +(the tree's active frames, itself included; the precheck passes 1, a Call passes `tp_fd + 1`). The +two points where a fast tree can run Python code, a `redo` Call and the loop poll's `tp_poll_slow`, +are wrapped in `tp_depth_py_begin(tp_fd)` / `tp_depth_py_end(tp_fd)`, so everything run from there +(interpreted frames, signal handlers, compiled calls through their precheck) sees the exact count. +`-DTP_TRACE_FAST` +(tests only) adds `__tp_trace_fast_hits__()` to the module: the number of entries that took the fast path. + +## Eval breaker (top of every loop iteration and impl entry; issue #141) + int tp_poll(PyObject *breaker); /* 0 continue; -1 exception set (a signal handler raised) */ + int tp_poll_slow(PyObject *breaker); /* every TP_POLL_INTERVAL polls: call the breaker */ + int tp_pc_tick(int *pc); /* loop countdown: 1 (and reset) when the slow path is due */ + Py_ssize_t tp_snapshot_depth; /* THREAD-LOCAL; ++ at entry, -- at tp_exit of a snapshot-holding impl */ + int tp_poll_countdown; /* THREAD-LOCAL */ +`breaker` is the module state's `lambda: None`. Calling it runs CPython's own eval-breaker handling +in its RESUME: signals, pending calls, stop-the-world requests of other threads (the free-threaded +GC), async exceptions and scheduled collections, exactly as between two bytecodes. A function that holds an array copy-in +(ir.ArrayParam) or an entry-globals snapshot never polls, and while one is active the slow path does +nothing: the event stays pending and is handled after it returns. + +## Fixed-layout classes (ir.ClassDecl / FieldGet / FieldSet / New / IsExact / CheckExact; SPEC N-11) +Guard state, one per ClassDecl in the module state (zeroed = not compiled): + + typedef struct { + unsigned int tag; /* ATOMIC. tp_version_tag at which the slots were proved; never 0 while compiled */ + unsigned int bad_tag; /* ATOMIC. a tag at which tp_class_refresh failed (0: none); not retried */ + uint64_t gepoch; /* ATOMIC. tp_globals_epoch at which module_dict[name] was last seen to be the class */ + PyObject *init; /* IMMUTABLE after capture. strong: the class's own __init__ at capture, or NULL */ + Py_ssize_t slow; /* ATOMIC. failed per-access checks (introspection) */ + Py_ssize_t refreshed; /* ATOMIC. new tags adopted by tp_class_refresh (introspection) */ + } tp_class_rt; + static uint64_t tp_globals_epoch; /* ATOMIC. moves when a watched module dict changes a class-name key */ + /* atomic accessors (relaxed loads, acq_rel adds); every read of cls->tp_version_tag goes through one */ + unsigned int tp_type_tag(const PyTypeObject *cls); + void tp_class_note_slow(tp_class_rt *rt); + unsigned int tp_class_rt_tag(const tp_class_rt *rt); + Py_ssize_t tp_class_rt_slow(const tp_class_rt *rt); + Py_ssize_t tp_class_rt_refreshed(const tp_class_rt *rt); + +At module init (none runs user code): + + /* `name` in cls's OWN dict (no MRO): 1 found (*out new ref), 0 absent, -1 error */ + int tp_own_attr(PyTypeObject *cls, const char *name, PyObject **out); + /* `field` in cls's own dict must be a member descriptor made for cls, of a writable + Py_T_OBJECT_EX slot of that name inside the instance: *out = its PyMemberDef, *descr = a new + reference to the descriptor. 0 ok, 1 not a plain slot (class not compiled), -1 error */ + int tp_slot_capture(PyTypeObject *cls, const char *field, PyMemberDef **out, PyObject **descr); + /* heap type made by `type`, base object, no __dict__, fixed size, object's tp_new/tp_alloc, + generic getattro/setattro (no __getattribute__/__getattr__/__setattr__/__delattr__), not abstract */ + int tp_class_shape_ok(PyTypeObject *cls); + /* the whole ClassDecl check: shape, every field (slots[i], descrs[i]), rt->init, and a version + tag (PyUnstable_Type_AssignVersionTag; none → not compiled). 1 compiled, 0 not, -1 error; + on 0/-1 nothing is kept */ + int tp_class_capture(PyObject *cls, const char *const *fields, Py_ssize_t n, + PyMemberDef **slots, PyObject **descrs, tp_class_rt *rt); + void tp_class_rt_clear(tp_class_rt *rt, PyObject **descrs, Py_ssize_t n); /* drop refs, zero tags */ + +At every access (the class may have changed since init; cgen.py "Classes changed after init"): + + /* FieldGet/FieldSet (obj) or New (obj == NULL): 1 = type(obj) is cls and cls's tp_version_tag is + rt->tag (or tp_class_refresh re-proved it), 0 = take the slow path / deopt, -1 = error. + Fast case: one type compare, one tag compare. */ + int tp_class_current(PyObject *obj, PyObject *cls, tp_class_rt *rt, const char *const *fields, + Py_ssize_t n, PyObject *const *descrs); + /* the tag moved: get a tag (exhausted → 0), shape still ok, every field still the very + descriptor object and __init__ the very object captured → adopt the tag (1); else remember + it in bad_tag (0). -1 error. Runs no user code. */ + int tp_class_refresh(PyTypeObject *cls, tp_class_rt *rt, const char *const *fields, + Py_ssize_t n, PyObject *const *descrs); + /* LOAD_GLOBAL of the class name as an identity test: 1 module_dict[name] is cls, 0 not (or not a + module global), -1 error. watched: a 1 is cached in rt->gepoch until tp_globals_epoch moves. + The slow path reads the epoch and looks the name up inside the module dict's critical section + (the dict watcher fires before a change is stored and under that same lock), so a binding + changed by another thread is never remembered as current. */ + int tp_class_global_ok(PyObject *module_dict, PyObject *name, PyObject *cls, tp_class_rt *rt, + int watched); + unsigned int tp_class_tag(PyObject *cls); /* cls->tp_version_tag; 0 for NULL */ + +Module-dict watcher (one id per module instance; ids 2..7 of 8 per interpreter are free for all +extensions together): + + /* body of the module's PyDict_WatchCallback: move tp_globals_epoch when key is an exact str + equal to one of `names` (NULL-terminated), any other key type or str subclass, or NULL + (cleared/cloned/deallocated). Never raises; returns 0 */ + int tp_globals_watch_event(PyDict_WatchEvent event, PyObject *key, const char *const *names); + /* PyDict_AddWatcher + PyDict_Watch: *id1 = id + 1, or 0 when no id is free / Watch failed + (error cleared: the caller uses the conservative path, a dict lookup per New) */ + void tp_globals_watch(PyObject *module_dict, PyDict_WatchCallback cb, int *id1); + /* PyDict_Unwatch (if module_dict) + PyDict_ClearWatcher; idempotent; keeps a pending exception */ + void tp_globals_unwatch(PyObject *module_dict, int *id1); + +Slot access and the slow paths: + + PyObject *tp_field_get(PyObject *obj, PyMemberDef *m); /* PyMember_GetOne: new ref or AttributeError */ + int tp_field_set(PyObject *obj, PyMemberDef *m, PyObject *v); /* PyMember_SetOne: 0 / -1 */ + int tp_setattr(PyObject *obj, PyObject *name, PyObject *v); /* STORE_ATTR (PyObject_SetAttr): 0 / -1 */ + PyObject *tp_new_fixed(PyTypeObject *cls, PyMemberDef *const *slots, PyObject *const *values, Py_ssize_t n); + /* cls->tp_alloc(cls, 0) then set each slot (new references); NULL + MemoryError */ + int tp_is_exact(PyObject *obj, PyTypeObject *cls); /* Py_IS_TYPE */ +FieldGet's slow path is `tp_getattr`, New's is `tp_global` + `tp_call` (the binding loaded before +the arguments), IsExact's compares with `tp_global`; in a pure function every slow path is a deopt. + +Version tags (CPython 3.14 Objects/typeobject.c): PyType_Modified sets tp_version_tag to 0 for the +type and its subclasses; assign_version_tag gives a heap type `NEXT_VERSION_TAG(interp)++`, a +per-interpreter counter that only grows and, once it wraps to 0, assigns nothing again; a class +gets at most MAX_VERSIONS_PER_CLASS (1000) tags. So a tag never repeats within an interpreter and a +class whose tags are exhausted keeps 0 (never current). Py_TPFLAGS_VALID_VERSION_TAG is "Unused. +Legacy flag" in 3.14 (never set) and is not consulted. + +## Free-threaded CPython (3.15t; issue #159) +Generated modules declare `{Py_mod_gil, Py_MOD_GIL_NOT_USED}` (under `#ifdef Py_GIL_DISABLED`), so +importing one keeps the GIL off. Every piece of shared mutable state, classified (the audit; a new +static or module-state field must be added here): + +| State | Where | Class | How | +|---|---|---|---| +| `tp_depth` (compiled frames in flight, cached frame) | tp_runtime.h static | thread-local | `TP_THREAD_LOCAL` | +| `tp_poll_countdown` | tp_runtime.h static | thread-local | `TP_THREAD_LOCAL` | +| `tp_snapshot_depth` | tp_runtime.h static | thread-local | `TP_THREAD_LOCAL`: one thread's array function never silences another thread's polls | +| `tp_pc` (loop countdown) | local of each impl | per call | a C local | +| `tp_globals_epoch` | tp_runtime.h static | atomic | relaxed loads, acq_rel add in the watcher callback (any thread); the epoch + dict lookup that remember a binding run in the module dict's critical section | +| `cls_rt[k].tag`, `.bad_tag`, `.gepoch` | module state | atomic | relaxed `tp_atomic_*`; a stale tag is never reused (tags only grow), so a lost or reordered refresh costs one more refresh | +| `cls_rt[k].slow`, `.refreshed` | module state | atomic | `tp_atomic_add_ssize` (introspection only) | +| `cls->tp_version_tag` | CPython type | atomic read | `tp_type_tag` (CPython clears and reassigns it from any thread) | +| `cls_rt[k].init`, `names`, `kwnames`, `classes`, `cls_ok`, `slots`, `descrs`, `breaker`, `watch_id1` | module state | immutable after init | written by `tp_exec` (and `tp_clear` at teardown, when no call can run); the `PyMemberDef`s live in the classes the state holds | +| `tp_name_strings`, `tp_fields_*`, `tp_class_names`, `tp_source*`, `tp_methods`, `tp_slots`, `tp_moduledef` | generated statics | immutable | never written after import | +| `__typedpython_interpreted__` | module dict entry | immutable after init | the dict is filled by `tp_exec` only; read with `PyDict_GetItemRef` | +| `__typedpython_deopts__` | module dict entry | critical section | the read-add-write of `tp_cg_redo` runs in `Py_BEGIN_CRITICAL_SECTION(module dict)`: concurrent deopts lose no count | +| `buf_types` (array.array) | module state | immutable after init | written by `tp_exec` (and `tp_clear`); read-only type pointers compared by identity | +| buffer arguments of array parameters | caller's array.array memory | atomic | relaxed 8-byte `__atomic_load_n` / `__atomic_store_n` per element; the Py_buffer export is held for the call (open point on concurrent resize, #149) | +| list arguments of array parameters | caller's lists | critical section | `tp_cs_begin` before the copy-in, `tp_cs_end` after the write-back, on every path | +| module globals (class names, entry globals) | module dict | strong reads | `tp_global` and `tp_class_global_ok` use `PyDict_GetItemRef` | +| a compiled class's instances | other threads' objects | CPython | `PyMember_GetOne` / `SetOne` are what the interpreter's member descriptors call (they take the object's lock on free-threaded builds); `tp_new_fixed` writes only its own fresh object | +| `errno` (math helpers) | C library | thread-local | per thread by POSIX | + +Borrowed references are never held across anything another thread can free. Replaced in this change: +`tp_global`: `PyDict_GetItemWithError` (module globals, builtins) and `PyDict_GetItemString` +(`__builtins__`) became `PyDict_GetItemRef` / `PyDict_GetItemStringRef`; the builtins dict taken from +a module is kept alive by an own reference while it is read; `PyEval_GetBuiltins` became +`PyEval_GetFrameBuiltins` (a new reference). `tp_class_global_ok`: `PyDict_GetItemWithError` became +`PyDict_GetItemRef` inside the dict's critical section. `tp_cg_redo`: the counter update is under the +module dict's critical section. `PyList_GET_ITEM` in `tp_*_array_enter` stays a borrow: it is read +under the list's critical section the caller holds. Borrows that cannot race: `PyModule_GetDict` (the +dict lives as long as the module, which the running function keeps alive), `PyTuple_GET_SIZE` of a +kwnames tuple, `PyTuple` items. + +Memory orders: relaxed loads and stores for the tags and the epoch (the synchronisation that +publishes a class or global change to another thread, a lock or an event, orders them), acquire-release +for the epoch's increment. `tp_runtime.h` needs the GCC/Clang `__atomic` builtins. + +## Development-mode import gate (Python API, not C; SPEC N-7b) + typedpython.importgate.install(source_roots) # meta path finder; checks project modules before they run + typedpython.importgate.uninstall() + typedpython.importgate.install_from_env() # installs when TYPEDPYTHON_IMPORT_GATE= +A type error raises `ImportGateError` (an `ImportError`, `.diagnostics` holds the gate's errors) and the module's code does not run. Only files under the roots are checked; results are cached by content hash. Do not call it in release builds. diff --git a/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h b/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h index 2f64563d8..7521ff6d9 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h +++ b/python-multiplatform-ksp/src/main/python/typedpython/runtime/tp_runtime.h @@ -22,6 +22,92 @@ #error "tp_runtime.h needs CPython 3.12 or newer (PyErr_GetRaisedException)" #endif +/* ------------------------------------------------------------------------------------------ + * Free-threaded CPython (3.15t is the only supported build; issue #159, runtime/API.md + * "Free-threaded CPython"). Without a GIL every static, every module-state field and every + * object another thread can reach is shared. Each piece of shared mutable state is one of: + * thread-local TP_THREAD_LOCAL (tp_depth, tp_poll_countdown, tp_snapshot_depth) + * atomic tp_atomic_* below (tp_globals_epoch, tp_class_rt tag / bad_tag / gepoch / + * counters, tp_version_tag reads) + * immutable written only by the module's exec slot (or by tp_clear, when no call can run) + * locked a critical section on the object (array parameters, the module dict, the + * deopt counter) + * and a borrowed reference is never held across anything another thread could free: containers + * are read through the strong-reference API (PyDict_GetItemRef ...) or under a critical section. + * ------------------------------------------------------------------------------------------ */ +#if defined(__GNUC__) || defined(__clang__) +static inline unsigned int tp_atomic_load_u32(const unsigned int *p) +{ return __atomic_load_n(p, __ATOMIC_RELAXED); } +static inline void tp_atomic_store_u32(unsigned int *p, unsigned int v) +{ __atomic_store_n(p, v, __ATOMIC_RELAXED); } +static inline uint64_t tp_atomic_load_u64(const uint64_t *p) +{ return __atomic_load_n(p, __ATOMIC_RELAXED); } +static inline void tp_atomic_store_u64(uint64_t *p, uint64_t v) +{ __atomic_store_n(p, v, __ATOMIC_RELAXED); } +static inline void tp_atomic_add_u64(uint64_t *p, uint64_t v) +{ (void)__atomic_fetch_add(p, v, __ATOMIC_ACQ_REL); } +static inline Py_ssize_t tp_atomic_load_ssize(const Py_ssize_t *p) +{ return __atomic_load_n(p, __ATOMIC_RELAXED); } +static inline void tp_atomic_add_ssize(Py_ssize_t *p, Py_ssize_t v) +{ (void)__atomic_fetch_add(p, v, __ATOMIC_RELAXED); } +#else +#error "tp_runtime.h needs the GCC/Clang __atomic builtins (the build uses cc, see cbuild.py)" +#endif + +/* cls->tp_version_tag: CPython clears and reassigns it from any thread (PyType_Modified, under the + * type's lock); every read here is an atomic load, as CPython's own (FT_ATOMIC_LOAD_UINT_RELAXED). */ +static inline unsigned int tp_type_tag(const PyTypeObject *cls) +{ + return tp_atomic_load_u32(&cls->tp_version_tag); +} + +#if defined(_MSC_VER) +#define TP_THREAD_LOCAL __declspec(thread) +#else +#define TP_THREAD_LOCAL __thread +#endif + +/* Critical sections for array parameters (tp_cs_*): from before the copy-in to after the + * write-back, on every exit path. One list or two (CPython offers one- and two-object sections + * only; a function with more list parameters stays interpreted). With a GIL they are no-ops. */ +typedef struct { + int n; /* 0 = not held, 1 or 2 objects locked */ +#ifdef Py_GIL_DISABLED + PyCriticalSection2 cs; +#endif +} tp_cs; + +static inline void tp_cs_begin(tp_cs *c, PyObject *a, PyObject *b) +{ +#ifdef Py_GIL_DISABLED + if (b == NULL) { + PyCriticalSection_Begin((PyCriticalSection *)&c->cs, a); + c->n = 1; + } + else { + PyCriticalSection2_Begin(&c->cs, a, b); + c->n = 2; + } +#else + (void)a; + c->n = b == NULL ? 1 : 2; +#endif +} + +/* idempotent: a no-op when nothing is held */ +static inline void tp_cs_end(tp_cs *c) +{ +#ifdef Py_GIL_DISABLED + if (c->n == 1) { + PyCriticalSection_End((PyCriticalSection *)&c->cs); + } + else if (c->n == 2) { + PyCriticalSection2_End(&c->cs); + } +#endif + c->n = 0; +} + /* ------------------------------------------------------------------------------------------ * Exception messages that CPython changed between 3.13 and 3.14. * 3.13: "integer division or modulo by zero" (int //), "integer modulo by zero" (int %), @@ -57,6 +143,13 @@ #define TP_BINOP_TRUEDIV 3 #define TP_BINOP_FLOORDIV 4 #define TP_BINOP_MOD 5 +/* the augmented forms `a op= b` (PyNumber_InPlace*): ir.BinOp.inplace */ +#define TP_BINOP_IADD 6 +#define TP_BINOP_ISUB 7 +#define TP_BINOP_IMUL 8 +#define TP_BINOP_ITRUEDIV 9 +#define TP_BINOP_IFLOORDIV 10 +#define TP_BINOP_IMOD 11 /* ------------------------------------------------------------------------------------------ * Integers (int64_t) @@ -132,6 +225,38 @@ static inline int tp_neg_i64(int64_t a, int64_t *out) return 0; } +/* Proven i64 operations (ir.BinOp.proven / UnaryOp.proven): the verifier showed the result fits in + * i64, so the generated code does the arithmetic with no check (unsigned wrap-around, which is plain + * machine arithmetic and has no undefined behaviour). Built with -DTP_CHECK_PROVEN, each of them is + * the checked helper again and a wrong proof is a SystemError, never a deopt: that is for the tests + * and for debugging a verifier change. The macros use the generated function's `tp_s`, `tp_rc` and + * `tp_exit`. */ +static inline int64_t tp_padd_i64(int64_t a, int64_t b) { return (int64_t)((uint64_t)a + (uint64_t)b); } +static inline int64_t tp_psub_i64(int64_t a, int64_t b) { return (int64_t)((uint64_t)a - (uint64_t)b); } +static inline int64_t tp_pmul_i64(int64_t a, int64_t b) { return (int64_t)((uint64_t)a * (uint64_t)b); } +static inline int64_t tp_pneg_i64(int64_t a) { return (int64_t)(0 - (uint64_t)a); } +#ifdef TP_CHECK_PROVEN +#define tp_proven_fail() \ + do { \ + PyErr_SetString(PyExc_SystemError, "typedpython: proven operation overflowed"); \ + tp_rc = -1; \ + goto tp_exit; \ + } while (0) +#define tp_proven(op, a, b, t) \ + do { \ + tp_s = tp_##op##_i64((a), (b), &(t)); \ + if (tp_s != 0) tp_proven_fail(); \ + } while (0) +#define tp_proven_neg(a, t) \ + do { \ + tp_s = tp_neg_i64((a), &(t)); \ + if (tp_s != 0) tp_proven_fail(); \ + } while (0) +#else +#define tp_proven(op, a, b, t) ((t) = tp_p##op##_i64((a), (b))) +#define tp_proven_neg(a, t) ((t) = tp_pneg_i64(a)) +#endif + /* Python floor division: the quotient rounds toward negative infinity. */ static inline int tp_floordiv_i64(int64_t a, int64_t b, int64_t *out) { @@ -581,6 +706,16 @@ static inline int tp_unbox_bool(PyObject *o, int *out) * * enter: rc 0 -> the struct holds the copy; rc 1 or -1 -> the struct is zeroed (so exit is a safe * no-op) and nothing was changed. `list` is borrowed: the caller keeps the argument alive. + * + * Free-threaded (#159): the CALLER holds the list's critical section (tp_cs_begin) from before + * enter to after exit, on every exit path. enter and exit read and write the list's items through + * borrowed references (PyList_GET_ITEM) and rely on that lock: no other thread can change the list + * between the copy-in and the write-back, so the whole call is one atomic step as far as lists are + * concerned (CPython can produce that interleaving: every list operation takes the same lock). + * Liveness note (issue #152): a function holding an array does not poll the eval breaker, so a long + * compiled array loop delays other threads' stop-the-world requests (the free-threaded GC) until it + * returns; results are unaffected. Polls inside such functions need the re-validation decided in + * #152 (after a poll the critical section is re-taken and the list may have changed). * exit: writes the dirty slots back as new objects (PyList_SetItem steals the new reference and, on * failure, drops it), always frees, and zeroes the struct. On a write-back failure it stops, * frees, and returns -1 with that error set (MemoryError, or IndexError if the list shrank). @@ -737,6 +872,148 @@ static inline int tp_i64_array_exit(tp_i64_array *a) return rc; } +/* ------------------------------------------------------------------------------------------ + * Buffer arrays (issue #149; ir.ArrayParam.buffer): an exact array.array argument + * is NOT copied. tp_buf_enter takes one Py_buffer export and tp_buf_exit releases it; the wrapper + * calls exit on every path (ok, error, deopt), so the export, which forbids a resize of the + * array.array, is held exactly as long as the compiled call. + * + * enter(obj, exact, fmt1, fmt2, writable, b): rc 0 = held (b->data, b->len set); rc 1 = deopt, b + * is zeroed and nothing is held, nothing changed, no exception left; rc -1 = MemoryError set. + * Deopt (never memory access) unless ALL hold: `exact` is non-NULL and Py_IS_TYPE(obj, exact) + * (a subclass could override __getitem__), PyObject_GetBuffer succeeds with C_CONTIGUOUS | FORMAT + * (| WRITABLE when the function stores), ndim == 1, itemsize == 8, no suboffsets, shape[0] * 8 == + * view.len, the format is exactly fmt1 or fmt2 ("d"; "q" or "l": the 8-byte signed ints, a byte + * order prefix or an unsigned/other format fails), and the data is 8-byte aligned (the access is + * an 8-byte atomic). An empty buffer is held too (len 0, no element is ever touched). + * + * Element access (tp_buf_ld_* / tp_buf_st_*): one relaxed atomic 8-byte load or store of the bit + * pattern on the buffer memory, bit-cast with memcpy. Another thread may write the same element concurrently: that is a data race in the program, and + * the atomic makes it a torn-value-free one (an element is read or written whole), the closest a + * compiled loop gets to what the interpreter's own element read/write does. -DTP_BUFFER_PLAIN + * replaces the atomics with plain loads and stores; it exists only to measure what the atomics + * cost, it is not a supported configuration for threads. len is the export's shape[0], fixed for + * the call (the export forbids resizing). + * ------------------------------------------------------------------------------------------ */ +typedef struct { Py_buffer view; Py_ssize_t len; void *data; int held; } tp_buf; + +/* `mod.attr` as a new reference in *out; not importable (or no such attribute): *out stays NULL + * and the error is cleared, so every guard against it deopts. -1 only for MemoryError. */ +static inline int tp_import_attr(const char *mod, const char *attr, PyObject **out) +{ + PyObject *m = PyImport_ImportModule(mod); + *out = NULL; + if (m != NULL) { + *out = PyObject_GetAttrString(m, attr); + Py_DECREF(m); + } + if (*out == NULL) { + if (PyErr_ExceptionMatches(PyExc_MemoryError)) { + return -1; + } + PyErr_Clear(); + } + return 0; +} + +static inline void tp_buf_exit(tp_buf *b) +{ + if (b->held) { + PyBuffer_Release(&b->view); + } + memset(b, 0, sizeof(*b)); +} + +static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2, + int writable, tp_buf *b) +{ + int flags = PyBUF_C_CONTIGUOUS | PyBUF_FORMAT; + memset(b, 0, sizeof(*b)); + if (exact == NULL || !Py_IS_TYPE(obj, (PyTypeObject *)exact)) { + return 1; + } + if (writable) { + flags |= PyBUF_WRITABLE; + } + if (PyObject_GetBuffer(obj, &b->view, flags) != 0) { + if (PyErr_ExceptionMatches(PyExc_MemoryError)) { + return -1; + } + PyErr_Clear(); /* read-only, not contiguous, ...: the object path */ + memset(b, 0, sizeof(*b)); + return 1; + } + b->held = 1; + if (b->view.ndim != 1 || b->view.itemsize != 8 || b->view.shape == NULL + || b->view.suboffsets != NULL || b->view.shape[0] < 0 + || b->view.shape[0] * 8 != b->view.len || b->view.format == NULL + || !(strcmp(b->view.format, fmt1) == 0 || (fmt2 != NULL && strcmp(b->view.format, fmt2) == 0)) + || (writable && b->view.readonly) + || (b->view.shape[0] > 0 && (b->view.buf == NULL || ((uintptr_t)b->view.buf & 7) != 0))) { + tp_buf_exit(b); + return 1; + } + b->len = b->view.shape[0]; + b->data = b->view.buf; + return 0; +} + +/* index: a negative index counts from the end; out of range -> IndexError with the text CPython's + * own array.array uses ("array index out of range" / "array assignment index out of range") */ +static inline int tp_buf_slot(const tp_buf *b, int64_t i, int store, Py_ssize_t *slot) +{ + if (i < 0) { + i += (int64_t)b->len; + if (i < 0) goto out_of_range; + } + else if (i >= (int64_t)b->len) { + goto out_of_range; + } + *slot = (Py_ssize_t)i; + return 0; +out_of_range: + PyErr_SetString(PyExc_IndexError, + store ? "array assignment index out of range" : "array index out of range"); + return -1; +} + +#ifdef TP_BUFFER_PLAIN +static inline double tp_buf_ld_f64(const tp_buf *b, Py_ssize_t i) { return ((const double *)b->data)[i]; } +static inline int64_t tp_buf_ld_i64(const tp_buf *b, Py_ssize_t i) { return ((const int64_t *)b->data)[i]; } +static inline void tp_buf_st_f64(const tp_buf *b, Py_ssize_t i, double v) { ((double *)b->data)[i] = v; } +static inline void tp_buf_st_i64(const tp_buf *b, Py_ssize_t i, int64_t v) { ((int64_t *)b->data)[i] = v; } +#else +static inline double tp_buf_ld_f64(const tp_buf *b, Py_ssize_t i) +{ + uint64_t u = __atomic_load_n((const uint64_t *)b->data + i, __ATOMIC_RELAXED); + double d; + memcpy(&d, &u, sizeof d); + return d; +} + +static inline int64_t tp_buf_ld_i64(const tp_buf *b, Py_ssize_t i) +{ + uint64_t u = __atomic_load_n((const uint64_t *)b->data + i, __ATOMIC_RELAXED); + int64_t v; + memcpy(&v, &u, sizeof v); + return v; +} + +static inline void tp_buf_st_f64(const tp_buf *b, Py_ssize_t i, double v) +{ + uint64_t u; + memcpy(&u, &v, sizeof u); + __atomic_store_n((uint64_t *)b->data + i, u, __ATOMIC_RELAXED); +} + +static inline void tp_buf_st_i64(const tp_buf *b, Py_ssize_t i, int64_t v) +{ + uint64_t u; + memcpy(&u, &v, sizeof u); + __atomic_store_n((uint64_t *)b->data + i, u, __ATOMIC_RELAXED); +} +#endif + /* ------------------------------------------------------------------------------------------ * Local arrays (ir.NewArray / CopyArray): owned by the function, no list object behind them. * Same structs as above with list == NULL and dirty == NULL; the slot helpers work on them @@ -903,29 +1180,42 @@ static inline int tp_any_same(PyObject *const *objs, Py_ssize_t n) * its "__builtins__" (a module or a dict) is searched next, the interpreter's builtins if absent. */ static inline PyObject *tp_global(PyObject *module_dict, PyObject *name) { - PyObject *v, *builtins; - v = PyDict_GetItemWithError(module_dict, name); - if (v != NULL) { - Py_INCREF(v); + PyObject *v = NULL, *builtins = NULL; + int rc; + /* strong references only (PyDict_GetItemRef): another thread may rebind or delete the name, + * or replace __builtins__, at any moment */ + rc = PyDict_GetItemRef(module_dict, name, &v); + if (rc > 0) { return v; } - if (PyErr_Occurred()) { + if (rc < 0) { return NULL; } - builtins = PyDict_GetItemString(module_dict, "__builtins__"); /* borrowed, errors ignored */ + rc = PyDict_GetItemStringRef(module_dict, "__builtins__", &builtins); /* errors ignored */ + if (rc < 0) { + PyErr_Clear(); + builtins = NULL; + } if (builtins != NULL && PyModule_Check(builtins)) { - builtins = PyModule_GetDict(builtins); + PyObject *d = PyModule_GetDict(builtins); /* borrowed: owned by `builtins`, which we hold */ + Py_XINCREF(d); + Py_DECREF(builtins); + builtins = d; } if (builtins == NULL || !PyDict_Check(builtins)) { - builtins = PyEval_GetBuiltins(); + Py_XDECREF(builtins); + builtins = PyEval_GetFrameBuiltins(); /* new reference */ + if (builtins == NULL) { + PyErr_Clear(); + } } if (builtins != NULL) { - v = PyDict_GetItemWithError(builtins, name); - if (v != NULL) { - Py_INCREF(v); + rc = PyDict_GetItemRef(builtins, name, &v); + Py_DECREF(builtins); + if (rc > 0) { return v; } - if (PyErr_Occurred()) { + if (rc < 0) { return NULL; } } @@ -987,21 +1277,211 @@ static inline int tp_obj_to_f64(PyObject *obj, double *out) return 0; } -/* op is an ir.BinOpKind: TP_BINOP_ADD .. TP_BINOP_MOD (declaration order) */ +/* Fast-path guards (issue #150). A fast path may replace a generic C API call only when the + * operand is exactly the type for which the fast path is what the generic call ends up in: + * subclasses (which can override __getitem__, __setitem__, __add__, ...) always take the generic + * call. These two macros are the only places where "exactly" is decided; the mutation tests in + * test_str_dict.py widen each to PyDict_Check / PyUnicode_Check and must fail. */ +#define TP_FASTPATH_GUARD_DICT(o) PyDict_CheckExact(o) +#define TP_FASTPATH_GUARD_STR(o) PyUnicode_CheckExact(o) + +/* op is an ir.BinOpKind: TP_BINOP_ADD .. TP_BINOP_MOD (declaration order), or its in-place form + * TP_BINOP_IADD .. TP_BINOP_IMOD. `str + str` (both exactly str) is PyUnicode_Concat, which is what + * PyNumber_Add ends in for two strs (str has no nb_add, its sq_concat is PyUnicode_Concat), and + * what the interpreter's BINARY_OP_ADD_UNICODE calls. */ static inline PyObject *tp_binop_obj(PyObject *a, PyObject *b, int op) { switch (op) { - case TP_BINOP_ADD: return PyNumber_Add(a, b); + case TP_BINOP_ADD: + if (TP_FASTPATH_GUARD_STR(a) && TP_FASTPATH_GUARD_STR(b)) { + return PyUnicode_Concat(a, b); + } + return PyNumber_Add(a, b); case TP_BINOP_SUB: return PyNumber_Subtract(a, b); case TP_BINOP_MUL: return PyNumber_Multiply(a, b); case TP_BINOP_TRUEDIV: return PyNumber_TrueDivide(a, b); case TP_BINOP_FLOORDIV: return PyNumber_FloorDivide(a, b); case TP_BINOP_MOD: return PyNumber_Remainder(a, b); + case TP_BINOP_IADD: return PyNumber_InPlaceAdd(a, b); + case TP_BINOP_ISUB: return PyNumber_InPlaceSubtract(a, b); + case TP_BINOP_IMUL: return PyNumber_InPlaceMultiply(a, b); + case TP_BINOP_ITRUEDIV: return PyNumber_InPlaceTrueDivide(a, b); + case TP_BINOP_IFLOORDIV: return PyNumber_InPlaceFloorDivide(a, b); + case TP_BINOP_IMOD: return PyNumber_InPlaceRemainder(a, b); } PyErr_SetString(PyExc_SystemError, "tp_binop_obj: unknown operator"); return NULL; } +/* ------------------------------------------------------------------------------------------ + * str and dict code (issue #150; API.md "Objects: subscript, in, f-strings, iteration"). Each + * helper makes the C API call the interpreter's bytecode makes; the fast paths are guarded by the + * exact type (TP_FASTPATH_GUARD_*). All return new references or NULL / -1 with CPython's exception. + * ------------------------------------------------------------------------------------------ */ + +/* BINARY_OP []: PyObject_GetItem. For an object that is exactly a dict: PyDict_GetItemRef (a strong + * reference, free-threaded safe) and KeyError(key) when absent, which is what dict_subscript does + * for an exact dict (BINARY_OP_SUBSCR_DICT in the interpreter). */ +static inline PyObject *tp_getitem(PyObject *obj, PyObject *key) +{ + if (TP_FASTPATH_GUARD_DICT(obj)) { + PyObject *v = NULL; + int rc = PyDict_GetItemRef(obj, key, &v); + if (rc > 0) { + return v; + } + if (rc == 0) { + PyObject *exc = PyObject_CallOneArg(PyExc_KeyError, key); /* _PyErr_SetKeyError */ + if (exc != NULL) { + PyErr_SetObject((PyObject *)Py_TYPE(exc), exc); + Py_DECREF(exc); + } + } + return NULL; + } + return PyObject_GetItem(obj, key); +} + +/* STORE_SUBSCR: PyObject_SetItem; an exact dict's mp_ass_subscript is PyDict_SetItem. 0 / -1. */ +static inline int tp_setitem(PyObject *obj, PyObject *key, PyObject *value) +{ + if (TP_FASTPATH_GUARD_DICT(obj)) { + return PyDict_SetItem(obj, key, value); + } + return PyObject_SetItem(obj, key, value); +} + +/* CONTAINS_OP: PySequence_Contains(container, needle). 1 / 0 / -1. */ +static inline int tp_contains(PyObject *container, PyObject *needle) +{ + return PySequence_Contains(container, needle); +} + +static inline PyObject *tp_new_list(void) +{ + return PyList_New(0); +} + +static inline PyObject *tp_new_dict(void) +{ + return PyDict_New(); +} + +/* One f-string field (CONVERT_VALUE, then FORMAT_SIMPLE or FORMAT_WITH_SPEC): `conversion` is -1, + * 's', 'r' or 'a'; `spec` is NULL or the format spec str. */ +static inline PyObject *tp_format(PyObject *value, PyObject *spec, int conversion) +{ + PyObject *converted, *result; + switch (conversion) { + case -1: converted = Py_NewRef(value); break; + case 's': converted = PyObject_Str(value); break; + case 'r': converted = PyObject_Repr(value); break; + case 'a': converted = PyObject_ASCII(value); break; + default: + PyErr_SetString(PyExc_SystemError, "tp_format: unknown conversion"); + return NULL; + } + if (converted == NULL) { + return NULL; + } + if (spec == NULL && PyUnicode_CheckExact(converted)) { + return converted; /* FORMAT_SIMPLE: an exact str is its own format */ + } + result = PyObject_Format(converted, spec); + Py_DECREF(converted); + return result; +} + +/* BUILD_STRING: the join of n strs with the empty separator. */ +static inline PyObject *tp_build_string(PyObject *const *parts, Py_ssize_t n) +{ + PyObject *seq = tp_tuple(parts, n); + PyObject *empty, *result; + if (seq == NULL) { + return NULL; + } + empty = PyUnicode_New(0, 0); /* the empty-string singleton */ + if (empty == NULL) { + Py_DECREF(seq); + return NULL; + } + result = PyUnicode_Join(empty, seq); + Py_DECREF(empty); + Py_DECREF(seq); + return result; +} + +/* GET_ITER: PyObject_GetIter. */ +static inline PyObject *tp_get_iter(PyObject *obj) +{ + return PyObject_GetIter(obj); +} + +/* FOR_ITER: the next item (a new reference), or NULL with no error set at the end (a StopIteration + * is the end and is cleared), or NULL with the exception set. */ +static inline PyObject *tp_iter_next(PyObject *iter) +{ + return PyIter_Next(iter); +} + +/* UNPACK_SEQUENCE (no star) of `v` into the n slots `out`, each a new reference. An exact tuple of + * length n is taken directly (immutable, so the items cannot change under us); anything else goes + * through its iterator, with the interpreter's unpack_iterable errors. 0 ok; -1 error, with no + * reference left in `out`. */ +static inline int tp_unpack(PyObject *v, Py_ssize_t n, PyObject **out) +{ + PyObject *it, *w; + Py_ssize_t i; + if (PyTuple_CheckExact(v) && PyTuple_GET_SIZE(v) == n) { + for (i = 0; i < n; i++) { + out[i] = Py_NewRef(PyTuple_GET_ITEM(v, i)); + } + return 0; + } + it = PyObject_GetIter(v); + if (it == NULL) { + if (PyErr_ExceptionMatches(PyExc_TypeError) && Py_TYPE(v)->tp_iter == NULL + && !PySequence_Check(v)) { + PyErr_Format(PyExc_TypeError, "cannot unpack non-iterable %.200s object", + Py_TYPE(v)->tp_name); + } + return -1; + } + for (i = 0; i < n; i++) { + w = PyIter_Next(it); + if (w == NULL) { + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_ValueError, + "not enough values to unpack (expected %zd, got %zd)", n, i); + } + goto fail; + } + out[i] = w; + } + w = PyIter_Next(it); + if (w == NULL) { + if (PyErr_Occurred()) { + goto fail; + } + Py_DECREF(it); + return 0; + } + Py_DECREF(w); + if (PyList_CheckExact(v) || PyTuple_CheckExact(v) || PyDict_CheckExact(v)) { + Py_ssize_t ll = PyDict_CheckExact(v) ? PyDict_Size(v) : Py_SIZE(v); + PyErr_Format(PyExc_ValueError, "too many values to unpack (expected %zd, got %zd)", n, ll); + } + else { + PyErr_Format(PyExc_ValueError, "too many values to unpack (expected %zd)", n); + } +fail: + while (i > 0) { + Py_CLEAR(out[--i]); + } + Py_DECREF(it); + return -1; +} + /* Py_CLEAR: the only way generated code drops a reference */ static inline void tp_release(PyObject **slot) { @@ -1009,16 +1489,421 @@ static inline void tp_release(PyObject **slot) } /* ------------------------------------------------------------------------------------------ - * Call depth (issue #57): compiled frames have no Python frame, so a recursion made of them would - * neither raise RecursionError nor stop before the C stack ends. tp_enter_call / tp_leave_call - * bracket every compiled impl function. See API.md "Call depth". + * Fixed-layout classes (SPEC N-11, ir.ClassDecl; API.md "Fixed-layout classes"). A compiled class + * is a plain heap type whose instances keep every field in an object slot at a fixed offset; the + * slots are reached through the PyMemberDef of the member descriptor captured at module init. + * Nothing here runs user code: PyMember_GetOne/SetOne touch the slot only. + * + * A class can change after init (a property replaces a field, __init__ is replaced, the module + * global is rebound, an instance's __class__ is assigned). Compiled code therefore re-proves its + * preconditions at EVERY FieldGet / FieldSet / New: + * - the class is unchanged: cls->tp_version_tag equals the tag captured (or re-proved) for it. + * CPython clears the tag to 0 on every change of the type (PyType_Modified / + * type_modified_unlocked) and a later lookup assigns a NEW one from a per-interpreter counter + * that only grows and stops at 0 when exhausted (typeobject.c assign_version_tag), so an old + * tag never comes back and a tag of 0 never matches (the captured tag is never 0). + * (Py_TPFLAGS_VALID_VERSION_TAG is "Unused. Legacy flag" in 3.14's object.h and is never set: + * it cannot be part of the test.) + * - FieldGet / FieldSet: Py_IS_TYPE(obj, cls) (covers `obj.__class__ = Other`). + * - New: the module global still is the class (tp_class_global_ok). + * When a check fails the generated code takes a slow path with CPython's own semantics (or, in a + * pure function, deopts); see cgen.py "Fixed-layout classes". * ------------------------------------------------------------------------------------------ */ -#if defined(_MSC_VER) -#define TP_THREAD_LOCAL __declspec(thread) + +/* Per-class guard state, one per ClassDecl in the module state (zeroed = not compiled). */ +typedef struct { + unsigned int tag; /* tp_version_tag at which the slots were proved; never 0 while compiled. + ATOMIC (tp_atomic_*): any thread reads and refreshes it */ + unsigned int bad_tag; /* a tag at which tp_class_refresh failed (0: none); not retried. ATOMIC */ + uint64_t gepoch; /* tp_globals_epoch at which module_dict[name] was last seen to be cls. + ATOMIC */ + PyObject *init; /* strong: the class's own __init__ at capture, or NULL if it had none. + IMMUTABLE after tp_class_capture */ + Py_ssize_t slow; /* per-access checks that failed (introspection only). ATOMIC */ + Py_ssize_t refreshed; /* new tags adopted by tp_class_refresh (introspection only). ATOMIC */ +} tp_class_rt; + +/* Moves whenever a watched module dict changes a key that is (or may be) a compiled class name, is + * cleared, cloned or deallocated (tp_globals_watch_event). Static: one counter per extension, shared + * by every instance of the module in the process; an instance re-proves its own globals after any + * move. ATOMIC (tp_atomic_*): the watcher callback runs on whichever thread changes the dict, and + * the module declares Py_mod_gil = Py_MOD_GIL_NOT_USED. The module declares no + * Py_mod_multiple_interpreters slot, so CPython loads it only into interpreters sharing the main + * one. */ +static uint64_t tp_globals_epoch = 1; + +/* `name` in cls's OWN dict (not through the MRO). 1 found (*out a new reference), 0 absent, -1 error. */ +static inline int tp_own_attr(PyTypeObject *cls, const char *name, PyObject **out) +{ + PyObject *dict = PyType_GetDict(cls); + int found; + + *out = NULL; + if (dict == NULL) { + return -1; + } + found = PyDict_GetItemStringRef(dict, name, out); + Py_DECREF(dict); + return found; +} + +/* Look up `field` in cls's OWN dict (not through the MRO: a subclass or an instance attribute must + * not be consulted) and capture its PyMemberDef. It must be a member descriptor made for exactly + * this class (d_type == cls), of an object slot (Py_T_OBJECT_EX), writable, of that name, whose + * offset lies inside the instance. 0 = captured (*out set, *descr a new reference to the descriptor), + * 1 = not a plain slot (the class is not compiled), -1 = error. The PyMemberDef lives in + * cls->tp_members: the caller must keep `cls` alive for as long as the pointer is used (the + * generated module holds a strong reference). */ +static inline int tp_slot_capture(PyTypeObject *cls, const char *field, PyMemberDef **out, + PyObject **descr_out) +{ + PyObject *descr = NULL; + int found, rc = 1; + PyMemberDescrObject *md; + PyMemberDef *m; + + *descr_out = NULL; + found = tp_own_attr(cls, field, &descr); + if (found < 0) { + return -1; + } + if (found == 0) { + return 1; + } + if (Py_IS_TYPE(descr, &PyMemberDescr_Type)) { + md = (PyMemberDescrObject *)descr; + m = md->d_member; + if (md->d_common.d_type == cls && m != NULL && m->name != NULL + && strcmp(m->name, field) == 0 && m->type == Py_T_OBJECT_EX + && (m->flags & Py_READONLY) == 0 && m->offset >= (Py_ssize_t)sizeof(PyObject) + && m->offset + (Py_ssize_t)sizeof(PyObject *) <= cls->tp_basicsize) { + *out = m; + *descr_out = descr; /* the reference moves to the caller */ + return 0; + } + } + Py_DECREF(descr); + return rc; +} + +/* The type-level conditions of ClassDecl: a heap type made by `type` (no metaclass), base `object`, + * no instance __dict__, no variable size, object's own tp_new and tp_alloc (so tp_new_fixed is what + * `C(...)` does for a trivial __init__), and the generic attribute protocol (no __getattribute__, + * __getattr__, __setattr__ or __delattr__: those would run instead of, or after, the slot access). */ +static inline int tp_class_shape_ok(PyTypeObject *cls) +{ + return (cls->tp_flags & Py_TPFLAGS_HEAPTYPE) && Py_TYPE(cls) == &PyType_Type + && cls->tp_base == &PyBaseObject_Type && cls->tp_dictoffset == 0 + && cls->tp_itemsize == 0 && cls->tp_new == PyBaseObject_Type.tp_new + && cls->tp_alloc == PyType_GenericAlloc && !(cls->tp_flags & Py_TPFLAGS_IS_ABSTRACT) + && cls->tp_getattro == PyObject_GenericGetAttr + && cls->tp_setattro == PyObject_GenericSetAttr; +} + +static inline void tp_class_rt_clear(tp_class_rt *rt, PyObject **descrs, Py_ssize_t n) +{ + Py_ssize_t i; + for (i = 0; i < n; i++) { + Py_CLEAR(descrs[i]); + } + Py_CLEAR(rt->init); + rt->tag = 0; + rt->bad_tag = 0; + rt->gepoch = 0; +} + +/* At module init: the class object must be what ClassDecl promises (tp_class_shape_ok) and every + * field a plain slot. slots[i] receives the PyMemberDef of fields[i], descrs[i] a strong reference + * to its member descriptor, rt->init a strong reference to the class's own __init__ (or NULL), and + * rt->tag the class's version tag (PyUnstable_Type_AssignVersionTag assigns one if it has none; a + * class that cannot get a tag is not compiled). 1 = compiled, 0 = not (nothing kept), -1 = error + * (nothing kept). */ +static inline int tp_class_capture(PyObject *cls_obj, const char *const *fields, Py_ssize_t n, + PyMemberDef **slots, PyObject **descrs, tp_class_rt *rt) +{ + PyTypeObject *cls; + Py_ssize_t i; + int rc; + + if (!PyType_Check(cls_obj)) { + return 0; + } + cls = (PyTypeObject *)cls_obj; + if (!tp_class_shape_ok(cls)) { + return 0; + } + for (i = 0; i < n; i++) { + rc = tp_slot_capture(cls, fields[i], &slots[i], &descrs[i]); + if (rc != 0) { + tp_class_rt_clear(rt, descrs, i); + return rc < 0 ? -1 : 0; + } + } + if (tp_own_attr(cls, "__init__", &rt->init) < 0) { + tp_class_rt_clear(rt, descrs, n); + return -1; + } + if (!PyUnstable_Type_AssignVersionTag(cls) || tp_type_tag(cls) == 0) { + tp_class_rt_clear(rt, descrs, n); + return 0; + } + /* init time: the class has not been handed to compiled code yet, plain stores are enough (the + * import completing publishes them) */ + rt->tag = tp_type_tag(cls); + rt->bad_tag = 0; + rt->gepoch = 0; + rt->slow = 0; + rt->refreshed = 0; + return 1; +} + +/* The class's tag moved (or is 0). Re-prove what the captured slots rely on, without running user + * code: get a tag (none left: 0), the shape still holds, every field's own-dict entry is the very + * member descriptor object captured at init, and __init__ is the very object captured at init. Then + * adopt the new tag (1). Otherwise remember the tag as bad (0), so the next access with that tag + * does not search again. -1 = error (a failed dict lookup). */ +static inline int tp_class_refresh(PyTypeObject *cls, tp_class_rt *rt, const char *const *fields, + Py_ssize_t n, PyObject *const *descrs) +{ + unsigned int tag; + Py_ssize_t i; + PyObject *d; + int found; + + if (!PyUnstable_Type_AssignVersionTag(cls) || tp_type_tag(cls) == 0) { + return 0; /* tags exhausted for this class */ + } + /* The tag is read BEFORE the dict is examined: a change after the read gives the class a new + * tag, so a tag adopted below is stale at once and the next access refreshes again. Two + * threads refreshing at the same time adopt the same or an older tag; both are safe. */ + tag = tp_type_tag(cls); + if (tag == tp_atomic_load_u32(&rt->tag)) { + return 1; + } + if (tag == tp_atomic_load_u32(&rt->bad_tag)) { + return 0; + } + if (!tp_class_shape_ok(cls)) { + goto bad; + } + for (i = 0; i < n; i++) { + found = tp_own_attr(cls, fields[i], &d); + if (found < 0) { + return -1; + } + Py_XDECREF(d); /* identity only; descrs[i] keeps it alive */ + if (d != descrs[i]) { + goto bad; + } + } + found = tp_own_attr(cls, "__init__", &d); + if (found < 0) { + return -1; + } + Py_XDECREF(d); + if (d != rt->init) { + goto bad; + } + tp_atomic_store_u32(&rt->tag, tag); + tp_atomic_add_ssize(&rt->refreshed, 1); + return 1; +bad: + tp_atomic_store_u32(&rt->bad_tag, tag); + return 0; +} + +/* A failed per-access check was taken (introspection counter). */ +static inline void tp_class_note_slow(tp_class_rt *rt) +{ + tp_atomic_add_ssize(&rt->slow, 1); +} + +/* introspection reads (__typedpython_class_info__) */ +static inline unsigned int tp_class_rt_tag(const tp_class_rt *rt) { return tp_atomic_load_u32(&rt->tag); } +static inline Py_ssize_t tp_class_rt_slow(const tp_class_rt *rt) { return tp_atomic_load_ssize(&rt->slow); } +static inline Py_ssize_t tp_class_rt_refreshed(const tp_class_rt *rt) +{ + return tp_atomic_load_ssize(&rt->refreshed); +} + +/* cls's current tp_version_tag, 0 for NULL (introspection: __typedpython_class_info__). */ +static inline unsigned int tp_class_tag(PyObject *cls) +{ + return cls != NULL ? tp_type_tag((PyTypeObject *)cls) : 0u; +} + +/* The per-access check of FieldGet / FieldSet (obj != NULL) and New (obj == NULL): 1 = the captured + * slots are what `obj.f` / `C(...)` reach now (obj is exactly cls, and cls is unchanged since the + * slots were proved, or re-proved by tp_class_refresh), 0 = take the slow path, -1 = error. The + * fast case is one type compare and one tag compare. */ +static inline int tp_class_current(PyObject *obj, PyObject *cls_obj, tp_class_rt *rt, + const char *const *fields, Py_ssize_t n, PyObject *const *descrs) +{ + PyTypeObject *cls = (PyTypeObject *)cls_obj; + if (cls == NULL || (obj != NULL && !Py_IS_TYPE(obj, cls))) { + return 0; + } + if (tp_type_tag(cls) == tp_atomic_load_u32(&rt->tag)) { /* rt->tag != 0 while cls != NULL */ + return 1; + } + return tp_class_refresh(cls, rt, fields, n, descrs); +} + +/* LOAD_GLOBAL of a class name, as an identity test: 1 = module_dict[name] is cls, 0 = it is not (or + * the name is not a module global: the slow path then asks tp_global), -1 = error. With a dict + * watcher on module_dict (`watched`), a 1 is remembered in rt->gepoch until tp_globals_epoch moves, + * so the steady state is one compare; without one (no watcher id was free) every call looks up. */ +static inline int tp_class_global_ok(PyObject *module_dict, PyObject *name, PyObject *cls, + tp_class_rt *rt, int watched) +{ + PyObject *v = NULL; + uint64_t epoch; + int rc; + if (cls == NULL) { + return 0; + } + if (watched && tp_atomic_load_u64(&rt->gepoch) == tp_atomic_load_u64(&tp_globals_epoch)) { + return 1; + } + /* The epoch is read and the dict looked up inside the dict's critical section. The watcher + * runs BEFORE a change is stored and under the same lock, so either the change finished (the + * lookup sees it) or it has not begun (its epoch move comes later and invalidates what is + * remembered below). Without the lock a thread could read the epoch after the move but look up + * before the store, and remember a stale binding as current for good. */ + Py_BEGIN_CRITICAL_SECTION(module_dict); + epoch = tp_atomic_load_u64(&tp_globals_epoch); + rc = PyDict_GetItemRef(module_dict, name, &v); /* strong */ + Py_END_CRITICAL_SECTION(); + if (rc <= 0) { + return rc; /* 0 absent, -1 error */ + } + rc = (v == cls); + Py_DECREF(v); + if (rc && watched) { + tp_atomic_store_u64(&rt->gepoch, epoch); + } + return rc; +} + +/* The body of a module's PyDict_WatchCallback: move tp_globals_epoch when `key` is one of the + * NULL-terminated class `names` (an exact str compared by content), may equal one (any other key + * type, a str subclass), or is NULL (cleared / cloned / deallocated). Never raises; returns 0. */ +static inline int tp_globals_watch_event(PyDict_WatchEvent event, PyObject *key, + const char *const *names) +{ + Py_ssize_t i; + (void)event; + if (key != NULL && PyUnicode_CheckExact(key)) { +#if PY_VERSION_HEX >= 0x030D0000 + for (i = 0; names[i] != NULL; i++) { + if (PyUnicode_EqualToUTF8(key, names[i])) { + tp_atomic_add_u64(&tp_globals_epoch, 1); + return 0; + } + } + return 0; #else -#define TP_THREAD_LOCAL __thread + (void)i; + (void)names; #endif + } + tp_atomic_add_u64(&tp_globals_epoch, 1); + return 0; +} + +/* Watch module_dict with `cb` (a function calling tp_globals_watch_event). *id1 = watcher id + 1, or + * 0 when no dict-watcher id is free (8 per interpreter, 2 reserved by CPython, shared with every + * other extension) or PyDict_Watch failed: the caller then takes the conservative path (a dict + * lookup per New). Never leaves an error set. */ +static inline void tp_globals_watch(PyObject *module_dict, PyDict_WatchCallback cb, int *id1) +{ + int id; + *id1 = 0; + id = PyDict_AddWatcher(cb); + if (id < 0) { + PyErr_Clear(); + return; + } + if (PyDict_Watch(id, module_dict) < 0) { + PyErr_Clear(); + if (PyDict_ClearWatcher(id) < 0) { + PyErr_Clear(); + } + return; + } + *id1 = id + 1; +} +/* Undo tp_globals_watch (module clear / free): unwatch module_dict (if still there) and release the + * watcher id. Idempotent; keeps any exception that was already set. */ +static inline void tp_globals_unwatch(PyObject *module_dict, int *id1) +{ + PyObject *exc; + if (*id1 <= 0) { + return; + } + exc = PyErr_GetRaisedException(); + if (module_dict != NULL && PyDict_Unwatch(*id1 - 1, module_dict) < 0) { + PyErr_Clear(); + } + if (PyDict_ClearWatcher(*id1 - 1) < 0) { + PyErr_Clear(); + } + *id1 = 0; + PyErr_SetRaisedException(exc); +} + +/* obj.field of a proved-exact instance: a new reference, or CPython's AttributeError for an unset + * slot ("'C' object has no attribute 'f'"). */ +static inline PyObject *tp_field_get(PyObject *obj, PyMemberDef *m) +{ + return PyMember_GetOne((const char *)obj, m); +} + +/* obj.field = v: the slot takes its own reference to v and releases the old one. 0 or -1. */ +static inline int tp_field_set(PyObject *obj, PyMemberDef *m, PyObject *v) +{ + return PyMember_SetOne((char *)obj, m, v); +} + +/* The slow path of FieldSet: STORE_ATTR (PyObject_SetAttr). 0 or -1. */ +static inline int tp_setattr(PyObject *obj, PyObject *name, PyObject *v) +{ + return PyObject_SetAttr(obj, name, v); +} + +/* The observable result of a trivial __init__: allocate with cls's tp_alloc and set slots[i] to + * values[i] (new references; the caller keeps its own). NULL with the error set on failure; the + * half-built object is released (its unset slots are NULL, which dealloc handles). */ +static inline PyObject *tp_new_fixed(PyTypeObject *cls, PyMemberDef *const *slots, + PyObject *const *values, Py_ssize_t n) +{ + Py_ssize_t i; + PyObject *obj = cls->tp_alloc(cls, 0); + + if (obj == NULL) { + return NULL; + } + for (i = 0; i < n; i++) { + if (PyMember_SetOne((char *)obj, slots[i], values[i]) < 0) { + Py_DECREF(obj); + return NULL; + } + } + return obj; +} + +/* type(obj) is cls */ +static inline int tp_is_exact(PyObject *obj, PyTypeObject *cls) +{ + return Py_IS_TYPE(obj, cls); +} + +/* ------------------------------------------------------------------------------------------ + * Call depth (issue #57): compiled frames have no Python frame, so a recursion made of them would + * neither raise RecursionError nor stop before the C stack ends. tp_enter_call / tp_leave_call + * bracket every compiled impl function. See API.md "Call depth". + * ------------------------------------------------------------------------------------------ */ typedef struct { Py_ssize_t count; /* compiled frames in flight on this thread (this module) */ PyObject *frame; /* strong ref: the top Python frame when `pydepth` was measured */ @@ -1076,4 +1961,133 @@ static inline void tp_leave_call(void) } } +/* Fast entry (issue #147). A *bounded* compiled function (cgen: no call cycle, bounded callees, no + * node that can run Python code) has a call tree of at most k compiled frames, k known at build time. + * `tp_depth_room(k)` is true exactly when tp_enter_call would accept all k nested frames, the same + * comparison with the deepest frame's count (depth + count + k <= limit); then the whole tree runs + * through its uncounted twins (`tp_fimpl_*`: no tp_enter_call, no tp_leave_call), which the C + * compiler can inline. When it is false the caller takes the counted path, so RecursionError is raised + * at exactly the call where it is raised without the fast entry, never earlier. + * + * Skipping Py_EnterRecursiveCall on the fast path is safe because the tree is bounded: at most k + * small compiled frames (no recursion, no Python callbacks except at the two points below), so they + * use a bounded amount of C stack on top of the entry's. + * + * The frames of a fast tree are not in `tp_depth.count`. A bounded tree runs Python code at exactly + * two points: a `redo` Call (the interpreted original) and the eval-breaker poll in a loop (signal + * handler, finalizer, another compiled call). Around each, the twin brackets the code with + * tp_depth_py_begin(tp_fd) / tp_depth_py_end(tp_fd), `tp_fd` being the tree's active frames (itself + * included), so the count is exactly what the counted path would have there: Python frames and + * compiled calls started from it see the true depth. */ +static inline int tp_depth_room(Py_ssize_t k) +{ + return tp_python_depth() + tp_depth.count + k <= Py_GetRecursionLimit(); +} + +/* After a fast tree: tp_python_depth may have cached the top frame (a strong reference) and, with + * no counted frame in flight, nothing else would drop it, as tp_leave_call does at count 0. */ +static inline void tp_depth_done(void) +{ + if (tp_depth.count == 0) { + Py_CLEAR(tp_depth.frame); + } +} + +/* Around a point of a fast tree that can run Python code: the count includes the tree's `fd` active + * frames. begin/end are balanced. end also drops the cached top frame when no compiled frame is + * counted any more (the code in between may have cached another frame, as tp_leave_call would). */ +static inline void tp_depth_py_begin(int fd) +{ + tp_depth.count += fd; +} + +static inline void tp_depth_py_end(int fd) +{ + tp_depth.count -= fd; + tp_depth_done(); +} + +#ifdef TP_TRACE_FAST +/* Test builds only (-DTP_TRACE_FAST): how many entries took the fast path. */ +static Py_ssize_t tp_trace_fast_hits_count = 0; +static inline void tp_trace_fast_hit(void) { tp_trace_fast_hits_count++; } +static PyObject *tp_trace_fast_hits(PyObject *tp_module, PyObject *tp_unused) +{ + (void)tp_module; (void)tp_unused; + return PyLong_FromSsize_t(tp_trace_fast_hits_count); +} +#else +static inline void tp_trace_fast_hit(void) {} +#endif + +/* ------------------------------------------------------------------------------------------ + * Eval breaker (issue #141). The interpreter checks between bytecodes for signals, pending calls, + * GIL drop requests, async exceptions and scheduled collections; compiled code has no bytecodes. + * So at the top of every loop iteration and at every impl entry `tp_poll` counts down a module + * counter (one decrement under the GIL), and every TP_POLL_INTERVAL polls `tp_poll_slow` calls a + * Python no-op (`lambda: None`, made by the module's exec slot). Its RESUME runs CPython's own + * eval-breaker handling, so everything the interpreter would do there happens, the same way + * (forced GIL switching to a waiting thread, KeyboardInterrupt, gc). Public API only. + * + * Signal handlers, pending calls and other threads are user code that may change lists and + * globals. A function holding an array copy-in or an entry-globals snapshot runs no user code by + * contract (ir.ArrayParam, ir.Function.entry_globals), so such a function does not poll, and while + * one is active (`tp_snapshot_depth > 0`, counted at its entry and exit) the slow path does nothing: + * the event stays pending and is handled at the next poll or bytecode after it returns, which is the + * CPython execution in which it arrived late. + * ------------------------------------------------------------------------------------------ */ +#ifndef TP_POLL_INTERVAL +#define TP_POLL_INTERVAL 4096 +#endif + +/* Per thread (#159): a shared counter would make threads poll for each other and, worse, let one + * thread's snapshot-holding function suppress another thread's polls. */ +static TP_THREAD_LOCAL int tp_poll_countdown = TP_POLL_INTERVAL; +static TP_THREAD_LOCAL Py_ssize_t tp_snapshot_depth = 0; + +/* Kept out of line and marked cold: inlined into every loop body it bloated short inner loops + * (fannkuch paid ~11% with it inline). */ +#if defined(__GNUC__) || defined(__clang__) +#define TP_COLD __attribute__((noinline, cold, unused)) +#define TP_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else +#define TP_COLD +#define TP_UNLIKELY(x) (x) +#endif + +static TP_COLD int tp_poll_slow(PyObject *breaker) +{ + PyObject *r; + tp_poll_countdown = TP_POLL_INTERVAL; + if (tp_snapshot_depth > 0 || breaker == NULL) { + return 0; + } + r = PyObject_CallNoArgs(breaker); + if (r == NULL) { + return -1; + } + Py_DECREF(r); + return 0; +} + +/* A loop's poll countdown (a local of the impl, kept in a register): 1 when the slow path is due, + * which also resets the countdown. Inline, with the rare case marked unlikely. */ +static inline int tp_pc_tick(int *pc) +{ + if (TP_UNLIKELY(--*pc == 0)) { + *pc = TP_POLL_INTERVAL; + return 1; + } + return 0; +} + +/* 0 = continue, -1 = exception set (e.g. KeyboardInterrupt from a signal handler). */ +static inline int tp_poll(PyObject *breaker) +{ + if (!TP_UNLIKELY(--tp_poll_countdown <= 0)) { + return 0; + } + return tp_poll_slow(breaker); +} + #endif /* TP_RUNTIME_H */ diff --git a/python-multiplatform-ksp/src/main/python/typedpython/verify.py b/python-multiplatform-ksp/src/main/python/typedpython/verify.py index e9b164799..b0611305e 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/verify.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/verify.py @@ -6,7 +6,7 @@ `Diagnostic`. A proved function comes back with each `Index`/`StoreIndex` whose bounds were proved marked `proven=True`; every other access has `proven=False` (an input `proven` is never trusted). -The verifier reads nothing but `ir` — it is independent of the front end — and it trusts no flag +The verifier reads nothing but `ir`, it is independent of the front end, and it trusts no flag the front end set (`pure`, `may_deopt`, `ArrayParam.stored`, node `type`s): each is recomputed from the body and compared. @@ -37,17 +37,80 @@ verify/structure break/continue outside a loop, the ForRange var assigned in its body, a zero or non-constant step, duplicate names, bad declarations, an entry global that collides with a parameter/local/other entry global or - is assigned in the body + is assigned in the body, or more than two array parameters (the lists + are locked from copy-in to write-back: one- and two-object critical + sections only, #159) verify/proven-overflow `proven` is set on an op the verifier cannot prove stays inside i64 (or on a node `proven` does not apply to, or it is not a bool) verify/redo `Call.redo` where it is not legal (see below) verify/entry-global-open a function with entry_globals is not closed + verify/array-open a function with an ArrayParam runs user code (#203) + verify/buffer a malformed `ArrayParam.buffer` (only None and "array"), or a + CopyArray of a buffer array (#149) + verify/class a class named by FieldGet/FieldSet/New/IsExact/CheckExact/Param.cls is not + a (valid) ClassDecl of the module, a field is not one of its fields, a + New names a class without trivial_init or has the wrong arity, or a + ClassDecl itself is malformed (bad name/fields, duplicates) + verify/field-unproved a FieldGet/FieldSet whose object is not proved to be exactly the class + +Fixed-layout classes (SPEC N-11). The verifier never trusts the front end for them either: + + * Every class named anywhere must be a valid `ClassDecl` of `Module.classes`; every field named + must be one of that class's `fields`; `New` needs `trivial_init` and exactly len(fields) OBJ + arguments; `Param.cls` only on an OBJ parameter (`optional` only together with `cls`). + * Exact-type proof for FieldGet/FieldSet (`verify/field-unproved` otherwise). The object must be + one of exactly these forms, for the same class C as the access: + (a) a `Local` that is a parameter with cls=C and optional=False, until the local is assigned; + (b) a `CheckExact(x, C)` node used directly as the object; + (c) a `New(C, ...)` node used directly as the object; + (d) a `Local` proved exact-C by control flow, with no assignment of it in between: inside the + `then` of `If(IsExact(Local, C), ...)`, or its `orelse` when the condition is false + (`Not(IsExact)`), and inside the right operand of an `And` whose left operand is + such a test (an `Or`'s right operand: when the left is false); or + `Is(Local x, Const(None, OBJ), negate=True)` (true, or `negate=False` false) when x is + an `optional` parameter of class C that the function never assigns anywhere; or + after `if : return/break/continue` (the facts of the branches that fall + through are intersected); or a local assigned from (b), (c) or another proved local + (`x = New(C, ..)`, `x = CheckExact(y, C)`, `x = y`) from there until it is assigned again; + (e) a `Local` that is not a parameter, entry global or loop var, and whose every Assign in the + function is a `New(C, ...)` of that same class: whatever is read from it (definite + assignment is proved separately) is a C created by New. Flow-insensitive, so one other kind + of assignment anywhere in the function ends the form. + Facts are killed by any assignment of the local (and, in a loop, by an assignment anywhere in + the loop body for the whole loop, the loop var included). Nothing else is accepted: not + isinstance, not a fact about another local, not a fact through a call, GetAttr or FieldGet. + * A `Call` of a compiled callee whose parameter has cls=C (the callee's entry guard) with an + argument not proved exact-C by the forms above can fail that guard in the middle of the caller: + it counts toward `may_deopt` (so it is legal only in a pure function) unless it is a legal redo. + * Purity/deopt: CheckExact can deopt (like Unbox to i64): only in a pure function, and it counts + toward `may_deopt` even when redundant. FieldSet is an effect (and makes the function not + *closed*: replacing a slot may drop the last reference to the old value, whose `__del__` is user + code). FieldGet, Is, IsExact, New are not effects. + * Typing: Is/IsExact give BOOL over OBJ operands; FieldGet/New/CheckExact give OBJ; `Const(None, + OBJ)` is the only OBJ constant (anything else `verify/type`) and counts as a reference creator + (a new reference to the None singleton) wherever an OBJ may flow: an operand of Is, a returned + value, an argument. + * Ownership: New, FieldGet and CheckExact are reference creators. CheckExact is read as the IR says, + "`obj` itself ... after checking": the result is a new reference to the operand (the back end + increfs it); the operand's own reference is untouched. + +Objects, str and dict code (#150). GetItem, Contains, Format, StoreItem and ForObj run user code +(`__getitem__`, `__hash__`, `__eq__`, `__format__`, `__iter__`, `__next__`, `__setitem__`): each is an +effect and makes the function not closed (so it can hold no array, no buffer and no entry global), +like CallObject. NewList, NewDict and BuildString run none. They are OBJ producers (new references) +with the OBJ operand slots below; an OBJ Const is None or a plain str (UTF-8, no NUL); a ForObj's +targets are distinct declared OBJ locals, assigned by the loop (so definitely assigned inside the +body, and not after it unless assigned before: a loop of zero iterations binds nothing), killing +any exact-type fact about them; `unpack` false means exactly one target; `BinOp.inplace` only on an +OBJ operation; `Format.conversion` is -1, 97, 114 or 115 and `spec` None or a plain str. Proven i64 operations. The verifier never trusts `BinOp.proven` / `UnaryOp.proven`: for every I64 expression it computes an interval [lo, hi] (exact Python ints) and accepts `proven=True` only on an -I64 ADD/SUB/MUL (BinOp) or NEG (UnaryOp) whose exact result interval lies inside [-2**63, 2**63-1]; -anything else (a different op, a non-I64 node, a result that may leave i64, a non-bool flag) is -rejected, and so is the whole function. A proven op is not a deopting node (an impure function may +I64 ADD/SUB/MUL (BinOp) or NEG (UnaryOp) whose exact result interval lies inside [-2**63, 2**63-1], +or on an I64 FLOORDIV/MOD whose divisor is a Const power of two >= 1 and whose dividend interval lies +inside [0, 2**63-1] (the back end then emits a shift / mask, #148); +anything else (a different op, a non-I64 node, a result that may leave i64, a dividend that may be +negative, another divisor, a non-bool flag) is rejected, and so is the whole function. A proven op is not a deopting node (an impure function may contain it; a pure one does not count it toward `may_deopt`); every unproven I64 ADD/SUB/MUL/NEG still deopts. The interval rules, exactly: @@ -57,17 +120,45 @@ step < 0 [min(hi(start), lo(stop) + 1), hi(start)] (the bounds are the intervals of the start/stop expressions, both evaluated before the loop; then intersected with the i64 range) + Local, any other I64 local see "Local variables and guards" below Len(array) [0, 2**62] ADD / SUB / MUL interval arithmetic on the operand intervals (for MUL the min/max of the four endpoint products); a proven op must fit i64 exactly, an unproven one is clamped to i64 (a result outside it deopts, so only in-range values flow on) NEG / POS [-hi, -lo] / [lo, hi] - everything else the full i64 range: I64 params and entry globals, every local that is - not the ForRange var of an enclosing loop (the verifier does no flow - analysis, so a local assigned a constant is still "any i64"), a loop - var after its loop, Index elements, Unbox, Call results, FLOORDIV, - MOD, TRUEDIV + MOD divisor interval > 0: [0, hi(b) - 1]; < 0: [lo(b) + 1, 0]; else + [-(d - 1), d - 1] with d = max |b| + FLOORDIV divisor interval that keeps its sign: the min/max of the four + corner quotients; else [-m, m] with m = max |a| (all clamped to i64: + -2**63 // -1 deopts, so it yields no value) + everything else the full i64 range: I64 params and entry globals, a loop var after + its loop, Index elements, Unbox, Call results, TRUEDIV + +Local variables and guards (#148). Two flow facts narrow what a read of an I64 local can be; both +are computed here, never taken from the front end. + + * Assigned values. A local that is not a parameter, entry global, array or ForRange var of the + function, and is assigned by Assign statements, has the join of the intervals of every value + assigned to it (an unproven op counts clamped to i64, as above). The intervals are a fixed point: + `_prove` walks the body again and again, each walk reading the intervals the previous one found, + until no interval grows; from the fourth walk a bound that still grows jumps to the i64 limit, + and after 16 walks every such local is full range. The fixed point means each assigned value + lies inside its local's interval, whichever path ran. An assignment that reads a local with no + interval yet (the first walks) adds nothing that walk. So `x = 1; x + 1` is proven, and a counter + that starts at 0 and only grows (`k = 0 ... k = k + 1`) is in [0, 2**63-1], not "any i64". + * Guards. Inside the then-branch of `If(c)` (the else-branch for `c` false), the body of + `While(c)`, and after an `If` one branch of which cannot complete (return, break, continue), a + Local that is a side of an I64 comparison with the other side's interval [lo, hi] is narrowed: + `x < e` gives x <= hi - 1, `x <= e` x <= hi, `x > e` x >= lo + 1, `x >= e` x >= lo, `x == e` + [lo, hi]; the Local may be either side; And (true) and Or (false) apply both links, Not flips, + the negation of a comparison is the opposite comparison, `!=` and anything else give nothing. + A guard on `x` ends at the next Assign to `x`; entering a loop drops the guards on every local + the loop body assigns (the back edge brings other values); a While's condition is re-read at + each iteration, so it narrows the top of every iteration. A read is the assigned-values interval + narrowed by the guard (and a ForRange var's interval is narrowed by it too). + So `if n > 0: return n - 1` and `while lo < hi: lo += 1; hi -= 1` need no check, `n <= 0` early + return then `n - 1` likewise. Local arrays (NewArray / CopyArray, ir.Function "Local arrays"). A local of an array type holds a native array the function owns. The verifier proves, never trusting the front end: @@ -97,12 +188,12 @@ Tuple. `Tuple(OBJ, elements)`: a tuple of OBJ expressions (a scalar needs Box), result OBJ, a reference creator in the ownership rule. Building one is not an effect. -What this cannot prove: anything that needs knowing a value through assignments (accumulators, -`x = 1; x + 1`), relations between variables (`i < n` does not bound `i + n`), bounds that come from -guards or conditions (`if n < 100`), values through array elements, calls or Unbox, the result of -`//` and `%` (even by a constant), a loop var after the loop, or `range` bounds that are themselves -full-range (`range(n)` bounds the var only by i64). A function needing such a proof keeps its -checked, deopting ops (and is then legal only if pure). +What this cannot prove: relations between variables (`i < n` bounds `i`, not `i + n`, and says +nothing about `j` when `j` is not compared), the range of a counter that is bounded only by how +often a loop runs (`k += 1` in `while c`), values through array elements, calls or Unbox, a loop var +after the loop, a guard on a local the guarded code assigns before the use, or `range` bounds that +are themselves full-range (`range(n)` bounds the var only by i64). A function needing such a proof +keeps its checked, deopting ops (and is then legal only if pure). Call.redo. Legal only if the caller is impure, the callee is pure and may_deopt, and the callee returns F64, BOOL or NONE (`verify/redo` otherwise). A redo call is not a deopting node in the @@ -130,6 +221,8 @@ Local, MathCall, MathFunc, Module, NewArray, ObjToFloat, Or, Param, Return, StoreIndex, ToFloat, Truth, Tuple, Type, Unbox, UnaryOp, UnaryOpKind, While, ) +from .ir import CheckExact, ClassDecl, FieldGet, FieldSet, Is, IsExact, New +from .ir import BuildString, Contains, ForObj, Format, GetItem, NewDict, NewList, StoreItem DA = "verify/definite-assignment" MISSING_RETURN = "verify/missing-return" @@ -149,6 +242,10 @@ PROVEN = "verify/proven-overflow" REDO = "verify/redo" ENTRY_OPEN = "verify/entry-global-open" +ARRAY_OPEN = "verify/array-open" +BUFFER = "verify/buffer" +CLASS = "verify/class" +FIELD = "verify/field-unproved" @dataclass(frozen=True) @@ -163,13 +260,18 @@ class Diagnostic: # the one the back end implements). _EXPRS = frozenset({Const, Local, BinOp, UnaryOp, Compare, And, Or, ToFloat, Box, Unbox, MathCall, Call, Global, GetAttr, CallObject, Truth, CompareObj, ObjToFloat, Len, Index, - NewArray, CopyArray, Tuple}) -_STMTS = frozenset({Assign, StoreIndex, ExprStmt, If, While, ForRange, Return, Break, Continue}) + NewArray, CopyArray, Tuple, Is, IsExact, CheckExact, FieldGet, New, + GetItem, Contains, NewList, NewDict, Format, BuildString}) +_STMTS = frozenset({Assign, StoreIndex, ExprStmt, If, While, ForRange, Return, Break, Continue, + FieldSet, StoreItem, ForObj}) # Nodes whose OBJ result is a new reference (ir.py "ownership": Box, Global, GetAttr, CallObject, # OBJ BinOp, Tuple), plus reads of an OBJ local (the local owns its reference; the back end increfs or # borrows under that rule) and Calls of a compiled function returning OBJ (which returns a new # reference by the same rule). -_OBJ_PRODUCERS = frozenset({Box, Global, GetAttr, CallObject, BinOp, Local, Call, Tuple}) +_OBJ_PRODUCERS = frozenset({Box, Global, GetAttr, CallObject, BinOp, Local, Call, Tuple, New, + FieldGet, CheckExact, Const, # an OBJ Const is None or a str (e_Const) + GetItem, NewList, NewDict, Format, BuildString}) +_CONVERSIONS = (-1, 97, 114, 115) # Format.conversion: none, !a, !r, !s _VALUE_TYPES = (Type.I64, Type.F64, Type.BOOL, Type.OBJ) # what a local may hold _RETURN_TYPES = _VALUE_TYPES + (Type.NONE,) _ELEMENT = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64} @@ -180,6 +282,15 @@ class Diagnostic: _FULL = (_I64_MIN, _I64_MAX) _LEN_MAX = 2 ** 62 _PROVABLE_OPS = frozenset({BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL}) +_POW2_OPS = frozenset({BinOpKind.FLOORDIV, BinOpKind.MOD}) # `proven` means shift / mask +_NEGATE = {CompareKind.EQ: CompareKind.NE, CompareKind.NE: CompareKind.EQ, + CompareKind.LT: CompareKind.GE, CompareKind.GE: CompareKind.LT, + CompareKind.LE: CompareKind.GT, CompareKind.GT: CompareKind.LE} +_SWAP = {CompareKind.EQ: CompareKind.EQ, CompareKind.NE: CompareKind.NE, + CompareKind.LT: CompareKind.GT, CompareKind.GT: CompareKind.LT, + CompareKind.LE: CompareKind.GE, CompareKind.GE: CompareKind.LE} +_ROUNDS = 16 # rounds of the local-interval fixpoint before every local is full range +_WIDEN_FROM = 3 # from this round on a bound that still grows jumps to the i64 limit # Definite assignment uses `None` for "unreachable" (bottom: every name counts as assigned). _State = frozenset[str] | None @@ -198,6 +309,7 @@ def __init__(self, var: str | None, bounds_array: str | None): self.var = var # ForRange var (None for While) self.bounds_array = bounds_array # array whose indices by `var` are in bounds self.interval = _FULL # the values `var` takes inside the body + self.bottom = False # fixpoint round: start/stop not known yet self.breaks: list[_State] = [] @@ -205,9 +317,26 @@ class _Checker: """Checks one function against the module's signatures. Collects diagnostics; rebuilds the body with proven bounds.""" - def __init__(self, function: Function, signatures: dict[str, Function]): + def __init__(self, function: Function, signatures: dict[str, Function], + classes: dict[str, ClassDecl] | None = None, + var_ivs: dict[str, tuple[int, int]] | None = None, fixing: bool = False): self.fn = function + # Local intervals (module docstring "Local variables and guards"): `var_ivs` is the interval + # of every eligible local; `fixing` says this walk is a round of the fixpoint that computes + # them (a local without an interval yet is then "bottom": `bottom` is set when one is read). + self.var_ivs = dict(var_ivs or {}) + self.fixing = fixing + self.eligible: frozenset[str] = frozenset() + self.contrib: dict[str, tuple[int, int]] = {} + self.bottom = False + self.dead = 0 # >0: inside code that is bottom in this round + self.refine: dict[str, tuple[int, int]] = {} # guard facts at this point self.sigs = signatures + self.classes = classes or {} + self.facts: dict[str, str] = {} # local -> class it is proved exactly (see docstring) + self.optional: dict[str, str] = {} # optional-class parameter -> class + self.reassigned: frozenset[str] = frozenset() + self.new_only: dict[str, str] = {} # local whose every assignment is New(C, ..) -> C self.diags: list[Diagnostic] = [] self.can_deopt = False self.effects: list[str] = [] # reasons the body is impure @@ -216,11 +345,13 @@ def __init__(self, function: Function, signatures: dict[str, Function]): self.loops: list[_Loop] = [] self.scalars: dict[str, Type] = {} # name -> type of every scalar/OBJ variable self.arrays: dict[str, Type] = {} # name -> array type of every ArrayParam + self.buffers: set[str] = set() # names of every buffer ArrayParam self.larrays: dict[str, Type] = {} # name -> array type of every local array self.assigns: dict[str, int] = {} # local array -> number of Assign statements to it self.lens: dict[str, tuple[int, int]] = {} # single-assignment local array -> length self.entry: dict[str, Type] = {} # name -> type of every entry global self.opens: list[str] = [] # reasons the function is not closed + self.slow_user: list[str] = [] # slow paths that can run user code (FieldGet, New) def error(self, rule: str, message: str) -> None: d = Diagnostic(self.fn.name, rule, message, self.fn.source_line) @@ -238,20 +369,29 @@ def declarations(self) -> frozenset[str]: self.error(TYPE, f"array parameter '{p.name}' has non-array type {p.type}") elif not isinstance(p.stored, bool): self.error(TYPE, f"array parameter '{p.name}' has a non-bool `stored`") + elif p.buffer not in (None, "array"): + self.error(BUFFER, f"array parameter '{p.name}' has buffer kind {p.buffer!r}") else: self.arrays[p.name] = p.type + if p.buffer: + self.buffers.add(p.name) elif type(p) is Param: if p.type not in _VALUE_TYPES: self.error(TYPE, f"parameter '{p.name}' has type {p.type}; arrays must be " f"ArrayParam") else: self.scalars[p.name] = p.type + self.param_class(p) else: self.error(UNKNOWN, f"parameter {p!r} is not an ir Param/ArrayParam") continue if p.name in params: self.error(STRUCTURE, f"parameter '{p.name}' appears twice") params.add(p.name) + if sum(type(p) is ArrayParam and p.buffer is None for p in fn.params) > 2: + self.error(STRUCTURE, "more than two array parameters: compiled code holds each list's " + "critical section from copy-in to write-back, and free-threaded " + "CPython has no section for more than two objects (#159)") eg = fn.entry_globals if not isinstance(eg, tuple): self.error(STRUCTURE, "entry_globals is not a tuple") @@ -267,6 +407,9 @@ def declarations(self) -> frozenset[str]: self.error(TYPE, f"entry global '{p.name}' has type {p.type}; only i64, f64 and " f"bool globals can be read at entry") continue + if p.cls is not None or p.optional is not False: + self.error(TYPE, f"entry global '{p.name}' carries a class annotation") + continue if p.name in params or p.name in self.entry: self.error(STRUCTURE, f"entry global '{p.name}' collides with a parameter or " f"another entry global") @@ -301,6 +444,102 @@ def declarations(self) -> frozenset[str]: self.error(TYPE, f"`{flag}` is not a bool") return frozenset(params) + def param_class(self, p: Param) -> None: + """`Param.cls`/`optional`: only on an OBJ parameter, naming a ClassDecl of the module. A + non-optional one is an exact-C fact at entry (the entry guard); an optional one is C or None.""" + if type(p.optional) is not bool: + self.error(TYPE, f"parameter '{p.name}' has a non-bool `optional`") + return + if p.cls is None: + if p.optional: + self.error(TYPE, f"parameter '{p.name}' is optional but names no class") + return + if p.type is not Type.OBJ: + self.error(TYPE, f"parameter '{p.name}' names class {p.cls!r} but is " + f"{p.type.value}, not obj") + elif not self.known_class(p.cls, f"parameter '{p.name}'"): + return + elif p.optional: + self.optional[p.name] = p.cls + else: + self.facts[p.name] = p.cls + + def known_class(self, cls: object, what: str) -> bool: + if not isinstance(cls, str) or cls not in self.classes: + self.error(CLASS, f"{what} names class {cls!r}, which is not a compiled class of this " + f"module") + return False + return True + + def known_field(self, cls: str, field: object, what: str) -> bool: + if not self.known_class(cls, what): + return False + if not isinstance(field, str) or field not in self.classes[cls].fields: + self.error(CLASS, f"{what}: '{field}' is not a field of class '{cls}' " + f"(fields: {', '.join(self.classes[cls].fields)})") + return False + return True + + # --- exact-type facts --------------------------------------------------------------------- + + def new_only_locals(self, params: frozenset[str]) -> dict[str, str]: + """Form (e): locals (not parameters, entry globals, loop vars or arrays) every Assign of + which is a `New` of one and the same declared class.""" + values: dict[str, list[object]] = {} + _assigned_values(self.fn.body, values) + loop_vars = _loop_vars(self.fn.body) + out: dict[str, str] = {} + for name, vs in values.items(): + if name in params or name in loop_vars or name in self.larrays: + continue + classes = {v.cls for v in vs if type(v) is New and isinstance(v.cls, str)} + if (len(classes) == 1 and all(type(v) is New for v in vs) + and next(iter(classes)) in self.classes): + out[name] = next(iter(classes)) + return out + + def exact_of(self, e: object) -> str | None: + """The class `e` is proved to be exactly (forms (a)-(d) of the module docstring).""" + cls = type(e) + if cls is Local: + return self.facts.get(e.name) or self.new_only.get(e.name) + if cls is CheckExact or cls is New: + return e.cls if isinstance(e.cls, str) and e.cls in self.classes else None + return None + + def _none_test(self, c: Is) -> str | None: + """`x is None` / `x is not None` on an optional-C parameter never assigned: the name.""" + for a, b in ((c.left, c.right), (c.right, c.left)): + if (type(a) is Local and type(b) is Const and b.type is Type.OBJ and b.value is None + and a.name in self.optional and a.name not in self.reassigned): + return a.name + return None + + def facts_if(self, c: object, truth: bool) -> dict[str, str]: + """The exact-type facts that hold when condition `c` evaluated to `truth`.""" + cls = type(c) + if cls is IsExact: + if truth and type(c.obj) is Local and c.cls in self.classes: + return {c.obj.name: c.cls} + return {} + if cls is Is and type(c.negate) is bool: + name = self._none_test(c) + if name is not None and truth is c.negate: # `is not None` true, or `is None` false + return {name: self.optional[name]} + return {} + if cls is UnaryOp and c.op is UnaryOpKind.NOT: + return self.facts_if(c.operand, not truth) + if (cls is And and truth) or (cls is Or and not truth): + return {**self.facts_if(c.left, truth), **self.facts_if(c.right, truth)} + return {} + + def proved(self, obj: ir.Expr, cls: str, what: str) -> None: + if self.exact_of(obj) != cls: + self.error(FIELD, f"{what}: the object is not proved to be exactly '{cls}' (needs an " + f"exact-{cls} parameter, a CheckExact, a New, or a dominating " + f"IsExact / `is not None` of an optional parameter on the same local, " + f"with no assignment in between)") + # --- expressions -------------------------------------------------------------------------- def slot(self, e: ir.Expr, want: Type, what: str) -> None: @@ -348,7 +587,9 @@ def e_Const(self, e: Const, state: _State) -> ir.Expr: or (t is Type.F64 and type(v) is float) or (t is Type.BOOL and type(v) is bool) or (t is Type.NONE and v is None)) - if not ok and t is not Type.OBJ: + if t is Type.OBJ: + ok = v is None or _plain_str(v) # None, or a str the C source can carry exactly + if not ok: self.error(TYPE, f"constant {v!r} does not match type {t.value}") return e @@ -383,19 +624,68 @@ def interval(self, e: ir.Expr) -> tuple[int, int]: if cls is Local: for loop in reversed(self.loops): if loop.var == e.name: - return loop.interval - return _FULL + if loop.bottom: + self.bottom = True + return _meet_interval(loop.interval, self.refine.get(e.name)) + base = _FULL + if e.name in self.eligible: + known = self.var_ivs.get(e.name) + if known is not None: + base = known + elif self.fixing: + self.bottom = True + return _meet_interval(base, self.refine.get(e.name)) if cls is Len: return self.lens.get(e.array, (0, _LEN_MAX)) if cls is BinOp and e.op in _PROVABLE_OPS: lo, hi = _exact(e.op, self.interval(e.left), self.interval(e.right)) return (max(lo, _I64_MIN), min(hi, _I64_MAX)) + if cls is BinOp and e.op in _POW2_OPS: + lo, hi = _div_interval(e.op, self.interval(e.left), self.interval(e.right)) + return (max(lo, _I64_MIN), min(hi, _I64_MAX)) if cls is UnaryOp and e.op in (UnaryOpKind.NEG, UnaryOpKind.POS): lo, hi = self.interval(e.operand) lo, hi = (-hi, -lo) if e.op is UnaryOpKind.NEG else (lo, hi) return (max(lo, _I64_MIN), min(hi, _I64_MAX)) return _FULL + def refinements(self, c: ir.Expr, truth: bool) -> dict[str, tuple[int, int]]: + """The intervals that hold of locals when condition `c` evaluated to `truth` (module + docstring "Local variables and guards"). A comparison of two I64 operands one of which is a + `Local` bounds that local by the interval of the other; And (true) / Or (false) combine + both links; Not flips; anything else gives nothing.""" + cls = type(c) + if cls is Compare and type(c.op) is CompareKind: + l, r = c.left, c.right + if getattr(l, "type", None) is not Type.I64 or getattr(r, "type", None) is not Type.I64: + return {} + kind = c.op if truth else _NEGATE[c.op] + out: dict[str, tuple[int, int]] = {} + if type(l) is Local: + _narrow(out, l.name, _bound(kind, self.interval(r))) + if type(r) is Local: + _narrow(out, r.name, _bound(_SWAP[kind], self.interval(l))) + return out + if cls is UnaryOp and c.op is UnaryOpKind.NOT: + return self.refinements(c.operand, not truth) + if (cls is And and truth) or (cls is Or and not truth): + out = self.refinements(c.left, truth) + for name, iv in self.refinements(c.right, truth).items(): + _narrow(out, name, iv) + return out + return {} + + def narrowed(self, c: ir.Expr, truth: bool) -> tuple[dict[str, tuple[int, int]], bool]: + """(the current guard facts narrowed by `c` being `truth`, whether the narrowing read a local + whose interval is not known yet in this fixpoint round).""" + self.bottom = False + found = self.refinements(c, truth) + bottom, self.bottom = self.bottom, False + out = dict(self.refine) + for name, iv in found.items(): + _narrow(out, name, iv) + return out, bottom + def check_proven(self, shape_ok: bool, interval: tuple[int, int], what: str) -> bool: """`proven=True` (already known to be a bool) is accepted only if re-proved. Returns whether the node counts as non-deopting.""" @@ -408,20 +698,43 @@ def check_proven(self, shape_ok: bool, interval: tuple[int, int], what: str) -> return True return False + def check_shift(self, op: BinOpKind, left: ir.Expr, right: ir.Expr) -> bool: + """`proven` on an I64 FLOORDIV / MOD: the divisor is a Const power of two >= 1 and the + dividend lies in [0, 2**63-1], so the back end may emit a shift / mask (Python's floor + semantics and C's agree there). Returns whether the node counts as non-deopting.""" + d = right.value if type(right) is Const and type(right.value) is int else None + if d is None or d < 1 or d & (d - 1) != 0: + self.error(PROVEN, f"`proven` is set on i64 {op.value} but the divisor is not a " + f"Const power of two >= 1") + elif self.interval(left)[0] < 0: + self.error(PROVEN, f"`proven` is set on i64 {op.value} by {d} but the dividend may be " + f"negative (its interval starts at {self.interval(left)[0]})") + else: + return True + return False + def e_BinOp(self, e: BinOp, state: _State) -> ir.Expr: if type(e.op) is not BinOpKind: self.error(UNKNOWN, f"BinOp op {e.op!r} is not a BinOpKind") return e left, right = self.expr(e.left, state), self.expr(e.right, state) lt, rt = getattr(left, "type", None), getattr(right, "type", None) + if type(e.inplace) is not bool: + self.error(TYPE, f"BinOp `inplace` {e.inplace!r} is not a bool") + elif e.inplace and lt is not Type.OBJ: + self.error(TYPE, "BinOp `inplace` is set on a non-OBJ operation (it is PyNumber_InPlace*)") proven = False if type(e.proven) is not bool: self.error(TYPE, f"BinOp `proven` {e.proven!r} is not a bool") elif e.proven: - shape = (e.type is Type.I64 and lt is Type.I64 and rt is Type.I64 - and e.op in _PROVABLE_OPS) - exact = _exact(e.op, self.interval(left), self.interval(right)) if shape else _FULL - proven = self.check_proven(shape, exact, f"{e.type.value} {e.op.value}") + both = e.type is Type.I64 and lt is Type.I64 and rt is Type.I64 + if both and e.op in _POW2_OPS: + proven = self.check_shift(e.op, left, right) + else: + shape = both and e.op in _PROVABLE_OPS + exact = (_exact(e.op, self.interval(left), self.interval(right)) + if shape else _FULL) + proven = self.check_proven(shape, exact, f"{e.type.value} {e.op.value}") if lt not in (Type.I64, Type.F64, Type.OBJ): self.error(TYPE if lt is not Type.OBJ else OWNERSHIP, f"BinOp {e.op.value} on {getattr(lt, 'value', lt)} operands") @@ -488,7 +801,12 @@ def e_Compare(self, e: Compare, state: _State) -> ir.Expr: def _bool_pair(self, e: And | Or, state: _State, cond: bool = False) -> ir.Expr: name = type(e).__name__ - left, right = self.expr(e.left, state, cond), self.expr(e.right, state, cond) + left = self.expr(e.left, state, cond) + saved = self.facts + # the right operand runs only when the left was true (And) / false (Or) + self.facts = {**saved, **self.facts_if(left, type(e) is And)} + right = self.expr(e.right, state, cond) + self.facts = saved self.slot(left, Type.BOOL, f"left operand of {name}") self.slot(right, Type.BOOL, f"right operand of {name}") if e.type is not Type.BOOL: @@ -586,6 +904,10 @@ def e_Call(self, e: Call, state: _State) -> ir.Expr: f"bool and none results are exactly reproducible") else: redo = True + if not redo: + for k, (a, p) in enumerate(zip(args, callee.params)): + if type(p) is Param and p.cls is not None and self.exact_of(a) != p.cls: + self.can_deopt = True # the callee's entry guard `type(x) is C` may fail if callee.may_deopt is True and not redo: self.can_deopt = True if not redo and not self.covers(callee): @@ -723,6 +1045,104 @@ def in_bounds(self, array: str, idx: ir.Expr) -> bool: return loop.bounds_array == array return False + # --- fixed-layout classes ----------------------------------------------------------------- + + def e_GetItem(self, e: GetItem, state: _State) -> ir.Expr: + obj = self.obj_operand(e.obj, state, "container of GetItem") + index = self.obj_operand(e.index, state, "index of GetItem") + self.result(e, Type.OBJ) + self.effects.append("GetItem (runs __getitem__, __hash__, __eq__, __index__)") + self.opens.append("GetItem") + return dataclasses.replace(e, obj=obj, index=index) + + def e_Contains(self, e: Contains, state: _State) -> ir.Expr: + left = self.obj_operand(e.left, state, "left operand of in") + right = self.obj_operand(e.right, state, "right operand of in") + if type(e.negate) is not bool: + self.error(TYPE, f"Contains `negate` {e.negate!r} is not a bool") + self.result(e, Type.BOOL) + self.effects.append("Contains (runs __contains__, __iter__, __eq__)") + self.opens.append("Contains") + return dataclasses.replace(e, left=left, right=right) + + def e_NewList(self, e: NewList, state: _State) -> ir.Expr: + self.result(e, Type.OBJ) + return e + + def e_NewDict(self, e: NewDict, state: _State) -> ir.Expr: + self.result(e, Type.OBJ) + return e + + def e_Format(self, e: Format, state: _State) -> ir.Expr: + operand = self.obj_operand(e.operand, state, "operand of Format") + if e.spec is not None and not _plain_str(e.spec): + self.error(TYPE, f"Format spec {e.spec!r} is not None or a plain str") + if e.conversion not in _CONVERSIONS or type(e.conversion) is not int: + self.error(TYPE, f"Format conversion {e.conversion!r} is not one of {_CONVERSIONS}") + self.result(e, Type.OBJ) + self.effects.append("Format (runs __format__, __str__, __repr__)") + self.opens.append("Format") + return dataclasses.replace(e, operand=operand) + + def e_BuildString(self, e: BuildString, state: _State) -> ir.Expr: + if not isinstance(e.elements, tuple) or len(e.elements) < 2: + self.error(STRUCTURE, "BuildString elements are not a tuple of at least two") + return e + elements = tuple(self.obj_operand(x, state, f"element {k + 1} of BuildString") + for k, x in enumerate(e.elements)) + for k, x in enumerate(elements): + if not (type(x) is Format or (type(x) is Const and type(x.value) is str)): + self.error(TYPE, f"element {k + 1} of BuildString is not a Format or a str Const") + self.result(e, Type.OBJ) + return dataclasses.replace(e, elements=elements) + + def e_Is(self, e: Is, state: _State) -> ir.Expr: + left = self.obj_operand(e.left, state, "left operand of is") + right = self.obj_operand(e.right, state, "right operand of is") + if type(e.negate) is not bool: + self.error(TYPE, f"Is `negate` {e.negate!r} is not a bool") + self.result(e, Type.BOOL) + return dataclasses.replace(e, left=left, right=right) + + def e_IsExact(self, e: IsExact, state: _State) -> ir.Expr: + obj = self.obj_operand(e.obj, state, "operand of IsExact") + self.known_class(e.cls, "IsExact") + self.result(e, Type.BOOL) + return dataclasses.replace(e, obj=obj) + + def e_CheckExact(self, e: CheckExact, state: _State) -> ir.Expr: + obj = self.obj_operand(e.obj, state, "operand of CheckExact") + self.known_class(e.cls, "CheckExact") + self.result(e, Type.OBJ) + self.can_deopt = True # a wrong type deopts, even if the check is redundant + return dataclasses.replace(e, obj=obj) + + def e_FieldGet(self, e: FieldGet, state: _State) -> ir.Expr: + obj = self.obj_operand(e.obj, state, f"object of .{e.field}") + self.result(e, Type.OBJ) + if self.known_field(e.cls, e.field, f"FieldGet .{e.field}"): + self.proved(obj, e.cls, f"FieldGet {e.cls}.{e.field}") + # a slot read runs no user code: not an effect. Its slow path after a class change (getattr) + # can, which matters to array functions (#203). + self.slow_user.append(f"FieldGet .{e.field} (generic getattr after a class change)") + return dataclasses.replace(e, obj=obj) + + def e_New(self, e: New, state: _State) -> ir.Expr: + self.slow_user.append(f"New {e.cls} (calls the class after a class change)") + args = self.args(e.args, state, f"New {e.cls}") + for k, a in enumerate(args): + self.slot(a, Type.OBJ, f"argument {k + 1} of New {e.cls}") + self.result(e, Type.OBJ) + if self.known_class(e.cls, "New"): + decl = self.classes[e.cls] + if decl.trivial_init is not True: + self.error(CLASS, f"New of '{e.cls}', whose __init__ is not trivial (it must be " + f"called as an object)") + if len(args) != len(decl.fields): + self.error(CLASS, f"New of '{e.cls}' has {len(args)} argument(s) for " + f"{len(decl.fields)} field(s)") + return dataclasses.replace(e, args=args) + # --- tuples and local arrays -------------------------------------------------------------- def e_Tuple(self, e: Tuple, state: _State) -> ir.Expr: @@ -761,6 +1181,9 @@ def allocation(self, e: NewArray | CopyArray, target: str, state: _State) -> ir. src = self.arrays.get(e.src) or self.larrays.get(e.src) if not isinstance(e.src, str) or src is None: self.error(ARRAY_USE, f"CopyArray of '{e.src}', which is not an array") + elif e.src in self.buffers: + self.error(BUFFER, f"CopyArray of buffer array '{e.src}': a buffer array is never " + "copied)") else: self.array(e.src, "copy", state) if src is not e.type: @@ -841,6 +1264,19 @@ def s_Assign(self, s: Assign, state: _State): self.error(TYPE, f"None assigned to '{target}'") if target in self.entry: self.error(STRUCTURE, f"entry global '{target}' is assigned in the body") + if (target in self.eligible and not alloc and not self.dead + and getattr(value, "type", None) is Type.I64): + self.bottom = False + iv = self.interval(value) + if not self.bottom: + self.contrib[target] = _join_interval(self.contrib.get(target), iv) + self.bottom = False + self.refine.pop(target, None) # a guard on `target` ends at its assignment + proved = None if alloc else self.exact_of(value) + if proved is not None: + self.facts[target] = proved + else: + self.facts.pop(target, None) for loop in self.loops: if loop.var == target: self.error(STRUCTURE, f"loop variable '{target}' is assigned in its loop body") @@ -864,20 +1300,128 @@ def cond(self, c: object, state: _State, what: str) -> ir.Expr: self.slot(c2, Type.BOOL, f"condition of {what}") return c2 + def s_FieldSet(self, s: FieldSet, state: _State): + obj = self.obj_operand(s.obj, state, f"object of .{s.field} =") + value = self.expr(s.value, state) + self.slot(value, Type.OBJ, f"value stored into .{s.field}") + if self.known_field(s.cls, s.field, f"FieldSet .{s.field}"): + self.proved(obj, s.cls, f"FieldSet {s.cls}.{s.field}") + self.effects.append(f"FieldSet .{s.field} (a store)") + self.opens.append(f"FieldSet .{s.field}") # dropping the old value may run its __del__ + return dataclasses.replace(s, obj=obj, value=value), state + + def s_StoreItem(self, s: StoreItem, state: _State): + obj = self.obj_operand(s.obj, state, "container of StoreItem") + index = self.obj_operand(s.index, state, "index of StoreItem") + value = self.obj_operand(s.value, state, "value of StoreItem") + self.effects.append("StoreItem (runs __setitem__, __hash__, __eq__)") + self.opens.append("StoreItem") + return dataclasses.replace(s, obj=obj, index=index, value=value), state + + def s_ForObj(self, s: ForObj, state: _State): + it = self.obj_operand(s.iter, state, "iterable of ForObj") + targets = s.targets + ok = (isinstance(targets, tuple) and targets and all(isinstance(t, str) for t in targets) + and len(set(targets)) == len(targets)) + if not ok: + self.error(STRUCTURE, f"ForObj targets {targets!r} are not distinct names") + targets = () + if type(s.unpack) is not bool: + self.error(TYPE, f"ForObj `unpack` {s.unpack!r} is not a bool") + elif not s.unpack and len(targets) != 1 and ok: + self.error(STRUCTURE, "ForObj without unpack has exactly one target") + for t in targets: + if t in self.arrays or t in self.larrays: + self.error(ARRAY_USE, f"ForObj target '{t}' is an array") + elif self.scalars.get(t) is not Type.OBJ: + self.error(TYPE, f"ForObj target '{t}' must be a declared obj local") + if t in self.entry: + self.error(STRUCTURE, f"entry global '{t}' is assigned by the loop") + for outer in self.loops: + if outer.var == t: + self.error(STRUCTURE, f"loop variable '{t}' is assigned in its loop body " + f"(by an inner loop)") + self.effects.append("ForObj (runs __iter__ and __next__)") + self.opens.append("ForObj") + loop = _Loop(None, None) + self.loops.append(loop) + head = self.loop_facts(s.body, tuple(targets)) + self.facts = dict(head) + killed = _assigned_names(s.body) | set(targets) + head_refine = {k: v for k, v in self.refine.items() if k not in killed} + self.refine = head_refine + body, _ = self.block(s.body, None if state is None else state | set(targets)) + self.loops.pop() + self.refine = dict(head_refine) + self.facts = dict(head) + # Zero iterations leave the targets as they were: the state after the loop is the entry + # state, met with every break state (each a superset of it). + after: _State = state + for br in loop.breaks: + after = _meet(after, br) + return dataclasses.replace(s, iter=it, body=body), after + def s_If(self, s: If, state: _State): cond = self.cond(s.cond, state, "if") + base = self.facts + entry = self.refine + self.facts = {**base, **self.facts_if(cond, True)} + self.refine, dead = self.narrowed(cond, True) + self.dead += dead then, st = self.block(s.then, state) + self.dead -= dead + then_facts, then_refine = self.facts, self.refine + self.facts = {**base, **self.facts_if(cond, False)} + self.refine = entry + self.refine, dead = self.narrowed(cond, False) + self.dead += dead orelse, se = self.block(s.orelse, state) + self.dead -= dead + else_facts, else_refine = self.facts, self.refine + if st is None and se is None: + self.refine = entry + elif st is None: + self.refine = else_refine + elif se is None: + self.refine = then_refine + else: + self.refine = {k: (min(v[0], else_refine[k][0]), max(v[1], else_refine[k][1])) + for k, v in then_refine.items() if k in else_refine} + if st is None and se is None: + self.facts = base + elif st is None: + self.facts = else_facts + elif se is None: + self.facts = then_facts + else: + self.facts = {k: v for k, v in then_facts.items() if else_facts.get(k) == v} return dataclasses.replace(s, cond=cond, then=then, orelse=orelse), _meet(st, se) + def loop_facts(self, body: object, extra: tuple[str, ...] = ()) -> dict[str, str]: + """The facts that hold at the head of every iteration: those of the entry that nothing in + the loop body (or its loop var) assigns.""" + killed = _assigned_names(body) | set(extra) + return {k: v for k, v in self.facts.items() if k not in killed} + def s_While(self, s: While, state: _State): + head = self.loop_facts(s.body) + self.facts = head + killed = _assigned_names(s.body) + head_refine = {k: v for k, v in self.refine.items() if k not in killed} + self.refine = head_refine cond = self.cond(s.cond, state, "while") loop = _Loop(None, None) + self.facts = {**head, **self.facts_if(cond, True)} + self.refine, dead = self.narrowed(cond, True) + self.dead += dead self.loops.append(loop) # Assignments only add names, so the loop-head state is the entry state (the meet of the # entry, end-of-body and continue states equals the entry state). body, _ = self.block(s.body, state) self.loops.pop() + self.dead -= dead + self.refine = dict(head_refine) + self.facts = dict(head) # leaving by the condition or a break: no more than the head's facts forever = type(s.cond) is Const and s.cond.type is Type.BOOL and s.cond.value is True after: _State = None if forever else state for br in loop.breaks: @@ -915,7 +1459,9 @@ def s_ForRange(self, s: ForRange, state: _State): loop = _Loop(var, bounds) if (step_ok and getattr(start, "type", None) is Type.I64 and getattr(stop, "type", None) is Type.I64): + self.bottom = False (slo, shi), (elo, ehi) = self.interval(start), self.interval(stop) + loop.bottom, self.bottom = self.bottom, False if step.value > 0: lo = slo hi = max(lo, ehi - 1) @@ -924,8 +1470,15 @@ def s_ForRange(self, s: ForRange, state: _State): lo = min(hi, elo + 1) loop.interval = (max(lo, _I64_MIN), min(hi, _I64_MAX)) self.loops.append(loop) + head = self.loop_facts(s.body, (var,)) + self.facts = dict(head) + killed = _assigned_names(s.body) | {var} + head_refine = {k: v for k, v in self.refine.items() if k not in killed} + self.refine = head_refine body, _ = self.block(s.body, None if state is None else state | {var}) self.loops.pop() + self.refine = dict(head_refine) + self.facts = dict(head) # Zero iterations leave `var` (and everything the body assigns) as it was: the state after # the loop is the entry state, met with every break state (each a superset of it). after: _State = state @@ -960,6 +1513,15 @@ def _jump(self, s: Break | Continue, state: _State): def run(self) -> Function: fn = self.fn params = self.declarations() + self.reassigned = frozenset(_assigned_names(fn.body)) + assigned: dict[str, list[object]] = {} + _assigned_values(fn.body, assigned) + loop_vars = _loop_vars(fn.body) + self.eligible = frozenset( + n for n in assigned + if self.scalars.get(n) is Type.I64 and n not in params and n not in loop_vars + and n not in self.arrays and n not in self.larrays) + self.new_only = self.new_only_locals(params) if self.larrays: counts: dict[str, int] = {} _assigned(fn.body, counts) @@ -1000,11 +1562,132 @@ def _assigned(body: object, counts: dict[str, int] | None = None) -> dict[str, i elif cls is If: _assigned(st.then, counts) _assigned(st.orelse, counts) - elif cls in (While, ForRange): + elif cls in (While, ForRange, ForObj): _assigned(st.body, counts) return counts +def _assigned_values(body: object, out: dict[str, list[object]]) -> None: + """Every Assign value in `body`, by target (tolerates a malformed body).""" + if isinstance(body, tuple): + for st in body: + cls = type(st) + if cls is Assign: + out.setdefault(st.target, []).append(st.value) + elif cls is If: + _assigned_values(st.then, out) + _assigned_values(st.orelse, out) + elif cls in (While, ForRange): + _assigned_values(st.body, out) + elif cls is ForObj: + for t in st.targets if isinstance(st.targets, tuple) else (): + out.setdefault(t, []).append(st) # not a New: ends form (e) for the target + _assigned_values(st.body, out) + + +def _loop_vars(body: object) -> set[str]: + out: set[str] = set() + if isinstance(body, tuple): + for st in body: + cls = type(st) + if cls is If: + out |= _loop_vars(st.then) | _loop_vars(st.orelse) + elif cls in (While, ForObj): + out |= _loop_vars(st.body) + elif cls is ForRange: + out.add(st.var) + out |= _loop_vars(st.body) + return out + + +def _assigned_names(body: object) -> set[str]: + """Every local assigned anywhere in `body` by an Assign or as a ForRange var.""" + out: set[str] = set() + if isinstance(body, tuple): + for st in body: + cls = type(st) + if cls is Assign: + out.add(st.target) + elif cls is If: + out |= _assigned_names(st.then) | _assigned_names(st.orelse) + elif cls is While: + out |= _assigned_names(st.body) + elif cls is ForObj: + out |= set(st.targets) if isinstance(st.targets, tuple) else set() + out |= _assigned_names(st.body) + elif cls is ForRange: + out.add(st.var) + out |= _assigned_names(st.body) + return out + + +def _plain_str(v: object) -> bool: + """A str the generated C source carries exactly: it encodes to UTF-8 and has no NUL.""" + if type(v) is not str or "\0" in v: + return False + try: + v.encode("utf-8") + except UnicodeEncodeError: + return False + return True + + +def _meet_interval(a: tuple[int, int], b: tuple[int, int] | None) -> tuple[int, int]: + """`a` narrowed by `b`; an empty meet (unreachable code) leaves `a`, which is still sound.""" + if b is None: + return a + lo, hi = max(a[0], b[0]), min(a[1], b[1]) + return (lo, hi) if lo <= hi else a + + +def _narrow(facts: dict[str, tuple[int, int]], name: str, iv: tuple[int, int] | None) -> None: + if iv is None: + return + cur = facts.get(name) + facts[name] = iv if cur is None else _meet_interval(cur, iv) + + +def _bound(kind: CompareKind, other: tuple[int, int]) -> tuple[int, int] | None: + """The interval of `x` when `x e` is true and `e` lies in `other`.""" + lo, hi = other + if kind is CompareKind.LT: + out = (_I64_MIN, hi - 1) + elif kind is CompareKind.LE: + out = (_I64_MIN, hi) + elif kind is CompareKind.GT: + out = (lo + 1, _I64_MAX) + elif kind is CompareKind.GE: + out = (lo, _I64_MAX) + elif kind is CompareKind.EQ: + out = (lo, hi) + else: + return None + return out if out[0] <= out[1] else None + + +def _div_interval(op: BinOpKind, a: tuple[int, int], b: tuple[int, int]) -> tuple[int, int]: + """The result interval of I64 FLOORDIV / MOD (an operation that completes: a zero divisor + raises and -2**63 // -1 deopts, so neither yields a value). MOD: [0, hi(b) - 1] for a positive + divisor, [lo(b) + 1, 0] for a negative one, else |result| < max |b|. FLOORDIV: for a divisor + that keeps its sign the extremes are at the corners; else |result| <= max |a|.""" + if op is BinOpKind.MOD: + if b[0] > 0: + return (0, b[1] - 1) + if b[1] < 0: + return (b[0] + 1, 0) + d = max(abs(b[0]), abs(b[1]), 1) + return (-(d - 1), d - 1) + if b[0] > 0 or b[1] < 0: + corners = [x // y for x in a for y in b] + return (min(corners), max(corners)) + m = max(abs(a[0]), abs(a[1])) + return (-m, m) + + +def _join_interval(a: tuple[int, int] | None, b: tuple[int, int]) -> tuple[int, int]: + return b if a is None else (min(a[0], b[0]), max(a[1], b[1])) + + def _exact(op: BinOpKind, a: tuple[int, int], b: tuple[int, int]) -> tuple[int, int]: """The exact (unbounded) result interval of ADD/SUB/MUL on operand intervals.""" if op is BinOpKind.ADD: @@ -1015,6 +1698,42 @@ def _exact(op: BinOpKind, a: tuple[int, int], b: tuple[int, int]) -> tuple[int, return min(products), max(products) +def _prove(g: Function, sigs: dict[str, Function], classes: dict[str, ClassDecl] + ) -> tuple[_Checker, Function]: + """Check `g`, first computing the interval of every eligible local (Local variables and + guards): walks of the body repeat, each reading the intervals found by the one before, until + they stop growing (a bound still growing from round `_WIDEN_FROM` jumps to the i64 limit). The + result is a fixed point, so every assigned value lies inside its local's interval. If it does + not settle in `_ROUNDS` walks, every eligible local is full range.""" + known: dict[str, tuple[int, int]] = {} + for rnd in range(_ROUNDS): + c = _Checker(g, sigs, classes, known, True) + rebuilt = c.run() + if not c.eligible: + return c, rebuilt + grown = dict(known) + for name in c.eligible: + iv = c.contrib.get(name) + if iv is None: + continue + old = known.get(name) + if old is None: + grown[name] = iv + continue + lo = old[0] if iv[0] >= old[0] else (_I64_MIN if rnd >= _WIDEN_FROM else iv[0]) + hi = old[1] if iv[1] <= old[1] else (_I64_MAX if rnd >= _WIDEN_FROM else iv[1]) + grown[name] = (lo, hi) + if grown == known: + absent = c.eligible - set(known) + if not absent: + return c, rebuilt + for name in absent: # never given an interval: nothing is known about it + grown[name] = _FULL + known = grown + c = _Checker(g, sigs, classes, {}, False) + return c, c.run() + + def verify(module: Module) -> tuple[Module, list[Diagnostic]]: """Prove each function of `module`; return the module of proved functions (with proven bounds marked) and every diagnostic. Rejected functions move to `skipped` with the reason.""" @@ -1041,19 +1760,22 @@ def reject(fn_name: str, line: int, rule: str, message: str) -> None: reject(name, g.source_line, STRUCTURE, f"function '{name}' is defined " f"{len(gs)} times") sigs = {name: gs[0] for name, gs in named.items() if len(gs) == 1} + classes, skipped_classes = _check_classes(module, set(named), diags) checked: dict[str, Function] = {} callees: dict[str, set[str]] = {} open_why: dict[str, str] = {} # a function that runs user code -> why + slow_why: dict[str, str] = {} # a function whose slow paths can run user code -> why for name, g in sigs.items(): - c = _Checker(g, sigs) - rebuilt = c.run() + c, rebuilt = _prove(g, sigs, classes) for d in c.diags: reject(d.function, d.source_line, d.rule, d.message) checked[name] = rebuilt callees[name] = c.callees if c.opens: open_why[name] = c.opens[0] + if c.slow_user: + slow_why[name] = c.slow_user[0] # Closedness (entry_globals): a fixpoint over the call graph. changed = True @@ -1070,6 +1792,26 @@ def reject(fn_name: str, line: int, rule: str, message: str) -> None: reject(name, g.source_line, ENTRY_OPEN, f"has entry globals but is not closed: {open_why[name]}") + # ArrayParam (#203): the list is copied in and written back, so nothing may run user code while + # the copy is live: user code could read or change the list. Closed as above, and also no + # FieldGet/New, whose slow paths run user code after a class change, anywhere in the call tree. + array_why = dict(open_why) + for name, why in slow_why.items(): + array_why.setdefault(name, why) + changed = True + while changed: + changed = False + for name, cs in callees.items(): + if name not in array_why: + bad = sorted(c for c in cs if c in array_why) + if bad: + array_why[name] = f"calls '{bad[0]}', which can run user code ({array_why[bad[0]]})" + changed = True + for name, g in sigs.items(): + if name in array_why and any(isinstance(p, ArrayParam) for p in g.params): + reject(name, g.source_line, ARRAY_OPEN, + f"holds a list copy-in but can run user code: {array_why[name]}") + # A call cycle (direct or mutual recursion) is legal: every compiled impl function counts its # frame at entry (tp_enter_call), so recursion ends in RecursionError where CPython raises it # and the C stack cannot overflow (issue #57). Purity and may_deopt flow through the cycle as @@ -1092,6 +1834,44 @@ def reject(fn_name: str, line: int, rule: str, message: str) -> None: kept = tuple(checked[g.name] for g in functions if type(g) is Function and g.name in checked and g.name not in reasons) skipped = dict(module.skipped) - for name, reason in reasons.items(): + for name, reason in {**skipped_classes, **reasons}.items(): skipped.setdefault(name, reason) - return Module(module.name, kept, skipped), diags + return Module(module.name, kept, skipped, tuple(classes.values())), diags + + +def _check_classes(module: Module, function_names: set[str], diags: list[Diagnostic] + ) -> tuple[dict[str, ClassDecl], dict[str, str]]: + """The valid ClassDecls of the module by name, and the reason for each invalid one.""" + declared = module.classes if isinstance(module.classes, tuple) else () + names: dict[str, int] = {} + for c in declared: + if type(c) is ClassDecl and isinstance(c.name, str): + names[c.name] = names.get(c.name, 0) + 1 + valid: dict[str, ClassDecl] = {} + bad: dict[str, str] = {} + for c in declared: + problem = None + if type(c) is not ClassDecl: + problem = f"{type(c).__name__} is not an ir ClassDecl" + elif not (isinstance(c.name, str) and c.name.isidentifier()): + problem = f"class name {c.name!r} is not an identifier" + elif names[c.name] > 1: + problem = f"class '{c.name}' is declared {names[c.name]} times" + elif c.name in function_names: + problem = f"class '{c.name}' has the name of a function of the module" + elif not (isinstance(c.fields, tuple) and all(isinstance(f, str) and f.isidentifier() + for f in c.fields)): + problem = f"fields of class '{c.name}' are not a tuple of identifiers" + elif len(set(c.fields)) != len(c.fields): + problem = f"class '{c.name}' lists a field twice" + elif type(c.trivial_init) is not bool: + problem = f"`trivial_init` of class '{c.name}' is not a bool" + name = getattr(c, "name", repr(c)) + if problem is None: + valid[c.name] = c + else: + d = Diagnostic(str(name), CLASS, problem, 0) + if d not in diags: + diags.append(d) + bad.setdefault(str(name), f"{CLASS}: {problem}") + return valid, bad diff --git a/python-multiplatform-ksp/src/test/python/conftest.py b/python-multiplatform-ksp/src/test/python/conftest.py index f04ff2974..139f2064e 100644 --- a/python-multiplatform-ksp/src/test/python/conftest.py +++ b/python-multiplatform-ksp/src/test/python/conftest.py @@ -28,7 +28,9 @@ def errors(diagnostics): # TP_SANITIZE=1 (tools/sanitizers.py): every extension TypedPython builds gets ASan + UBSan, and a -# sanitizer report fails the run (-fno-sanitize-recover=undefined; ASan halts). Nothing else changes. +# sanitizer report fails the run (-fno-sanitize-recover=undefined; ASan halts). It also defines +# TP_CHECK_PROVEN: an op the verifier proved runs the checked helper and raises SystemError if the +# proof was wrong (#148), so this run catches a wrong proof that the default build would let wrap. import os # noqa: E402 if os.environ.get("TP_SANITIZE") == "1": @@ -36,7 +38,7 @@ def errors(diagnostics): from typedpython import cbuild as _cbuild _SAN = ["-fsanitize=address,undefined", "-fno-sanitize-recover=undefined", "-g", - "-fno-omit-frame-pointer"] + "-fno-omit-frame-pointer", "-DTP_CHECK_PROVEN"] _orig_compiler = _cbuild.compiler def _sanitized_compiler(): diff --git a/python-multiplatform-ksp/src/test/python/runtime_shim/tp_shim.c b/python-multiplatform-ksp/src/test/python/runtime_shim/tp_shim.c index 2b6b07221..0f3030f35 100644 --- a/python-multiplatform-ksp/src/test/python/runtime_shim/tp_shim.c +++ b/python-multiplatform-ksp/src/test/python/runtime_shim/tp_shim.c @@ -608,4 +608,16 @@ static PyMethodDef methods[] = { static struct PyModuleDef moddef = {PyModuleDef_HEAD_INIT, TP_STR(TP_SHIM_NAME), NULL, -1, methods, NULL, NULL, NULL, NULL}; -PyMODINIT_FUNC TP_CAT(PyInit_, TP_SHIM_NAME)(void) { return PyModule_Create(&moddef); } +PyMODINIT_FUNC TP_CAT(PyInit_, TP_SHIM_NAME)(void) +{ + PyObject *m = PyModule_Create(&moddef); +#ifdef Py_GIL_DISABLED + /* #159: the helpers are thread-safe; do not let importing the shim switch the GIL back on for + the rest of the test process */ + if (m != NULL && PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED) < 0) { + Py_DECREF(m); + return NULL; + } +#endif + return m; +} diff --git a/python-multiplatform-ksp/src/test/python/test_buffer_arrays.py b/python-multiplatform-ksp/src/test/python/test_buffer_arrays.py new file mode 100644 index 000000000..6fa1e44c5 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_buffer_arrays.py @@ -0,0 +1,495 @@ +"""Zero-copy buffer-protocol array parameters (issue #149, identity-safe part; SPEC N-8 / N-9). + +A parameter annotated `array.array[float]` or `array.array[int]` is a *buffer array*: the entry +guard takes a `Py_buffer` export of the exact type, holds it until the function returns on every +exit, and every element access goes straight to the buffer memory as one relaxed atomic 8-byte load +or store. No copy-in, no write-back. + +numpy is not supported (#220): an `NDArray[...]` parameter is an ordinary object parameter and the +function runs as the interpreter runs it (one test below pins that). + +Everything here is single-threaded and differential against the interpreted function +(`__typedpython_interpreted__`): same result, same exception type and text, same final contents of +every argument. Tests that a path was *compiled* compare `__typedpython_deopts__` before and after, +so a function that silently stayed interpreted fails them. + +Not covered, on purpose: what a concurrent resize by another thread observes (open point of #149; +API.md "Buffer arrays"). +""" +from __future__ import annotations + +import array +import dataclasses +import importlib.machinery +import importlib.util +import textwrap + +import pytest + +from typedpython import cbuild, cgen, frontend, ir, verify + +SOURCE = textwrap.dedent(''' + # typedpython: compiled + import array + + + def sum_ad(a: array.array[float]) -> float: + s = 0.0 + for i in range(len(a)): + s += a[i] + return s + + + def sum_aq(a: array.array[int]) -> int: + s = 0 + for i in range(len(a)): + s += a[i] + return s + + + def get_ad(a: array.array[float], i: int) -> float: + return a[i] + + + def get_aq(a: array.array[int], i: int) -> int: + return a[i] + + + def set_ad(a: array.array[float], i: int, v: float) -> None: + a[i] = v + + + def set_aq(a: array.array[int], i: int, v: int) -> None: + a[i] = v + + + def length_ad(a: array.array[float]) -> int: + return len(a) + + + def scale_ad(a: array.array[float], k: float) -> None: + for i in range(len(a)): + a[i] = a[i] * k + + + def iota_aq(a: array.array[int]) -> None: + for i in range(len(a)): + a[i] = i + + + def copy_ad(dst: array.array[float], src: array.array[float]) -> None: + for i in range(len(dst)): + dst[i] = src[i] + + + def cascade_ad(a: array.array[float], b: array.array[float]) -> None: + for i in range(len(a)): + a[i] = b[i] + 1.0 + + + def fill_n(a: array.array[float], n: int) -> None: + for i in range(n): + a[i] = 1.0 + + + def mixed(l: list[float], a: array.array[float]) -> None: + for i in range(len(a)): + a[i] = l[i] + + + def four(a: array.array[float], b: array.array[float], c: array.array[float], + d: array.array[float]) -> None: + for i in range(len(a)): + a[i] = b[i] + c[i] + d[i] + + + def div_ad(a: array.array[float]) -> float: + return a[0] / a[1] + + + def open_ad(a: array.array[float], o: object) -> float: + return a[0] + float(o.v) + + + def rebind_view(a: array.array[float]) -> None: + y = a[:] + y[0] = 5.0 + + +''') + +COMPILED = { + "sum_ad", "sum_aq", "get_ad", "get_aq", "set_ad", "set_aq", "length_ad", "scale_ad", "iota_aq", + "copy_ad", "cascade_ad", "fill_n", "mixed", "four", "div_ad", +} +SKIPPED = {"open_ad", "rebind_view"} + + +def build_source(d, source, name="bufmod"): + path = d / f"{name}.py" + path.write_text(source) + proved, diags = verify.verify(frontend.lower(path)) + return path, proved, diags, cgen.generate(proved, path, path.name) + + +def load_so(so): + return cbuild.load(so.stem.split(".")[0], so) + + +@dataclasses.dataclass +class Built: + proved: object + diags: list + c: str + module: object + + +@pytest.fixture(scope="module") +def built(tmp_path_factory): + d = tmp_path_factory.mktemp("bufmod") + path, proved, diags, c = build_source(d, SOURCE) + so = cbuild.build(c, path.stem, d / "out") + return Built(proved, diags, c, load_so(so)) + + +def assert_compiled(built, *names): + have = {f.name for f in built.proved.functions} + assert set(names) <= have, (sorted(set(names) - have), built.proved.skipped) + + +def deopts(m): + return m.__typedpython_deopts__ + + +def interp(m, name): + return m.__typedpython_interpreted__[name] + + +def outcome(fn, args): + try: + return ("ok", fn(*args)) + except Exception as e: # noqa: BLE001 - the type and text are the observable + return ("raise", type(e), str(e)) + + +def same_value(a, b): + return a == b or (a != a and b != b) # nan + + +def differential(m, name, make, expect_deopt=False): + """Run the compiled and the interpreted function on two fresh copies of the arguments; assert + equal result (or equal exception type and text) and equal final contents of every argument.""" + ca, ia = make(), make() + before = deopts(m) + c = outcome(getattr(m, name), ca) + after = deopts(m) + i = outcome(interp(m, name), ia) + assert c[0] == i[0], (c, i) + if c[0] == "ok": + assert same_value(c[1], i[1]), (c, i) + else: + assert c[1:] == i[1:], (c, i) + for x, y in zip(ca, ia): + if isinstance(x, (array.array, list)): + assert len(x) == len(y) and all(same_value(p, q) for p, q in zip(x, y)), (x, y) + if expect_deopt: + assert after == before + 1, "expected a deopt (an object-path call), got none" + else: + assert after == before, "unexpected deopt: the compiled path did not run" + return c + + +F = [1.5, -2.25, 3.0, 4.5] +I = [3, -5, 7, 11] + + +def AD(v=F): return array.array("d", v) +def AQ(v=I): return array.array("q", v) + + +# --- what is compiled, and why the rest is not -------------------------------------------------- + +def test_compiled_set_and_skip_reasons(built): + names = {f.name for f in built.proved.functions} + assert names >= COMPILED, sorted(COMPILED - names) + for n in SKIPPED: + assert n in built.proved.skipped, n + assert "verify/array-open" in built.proved.skipped["open_ad"] + assert "never copied" in built.proved.skipped["rebind_view"] + + +def test_ir_shape_marks_buffer_params(built): + by = {f.name: f for f in built.proved.functions} + assert (by["sum_ad"].params[0].buffer, by["sum_ad"].params[0].stored) == ("array", False) + assert by["scale_ad"].params[0].stored is True + assert [q.buffer for q in by["mixed"].params] == [None, "array"] + + +def test_no_limit_on_buffer_parameters(built): + by = {f.name: f for f in built.proved.functions} + assert sum(p.buffer is not None for p in by["four"].params) == 4 + assert "tp_cs_begin" not in wrapper_text(built.c, "four") + n = 4 + a, b, c, d = (array.array("d", [float(k)] * n) for k in range(4)) + built.module.four(a, b, c, d) + assert a.tolist() == [6.0] * n + + +def wrapper_text(c, name): + start = c.index(f"static PyObject *tp_wrap_{name}(") + return c[start:c.index("\n}\n", start)] + + +# --- reads, writes, len --------------------------------------------------------------------------- + +@pytest.mark.parametrize("name,make", [ + ("sum_ad", lambda: (AD(),)), ("sum_aq", lambda: (AQ(),)), + ("sum_ad", lambda: (AD([]),)), ("sum_aq", lambda: (array.array("l", I),)), + ("length_ad", lambda: (AD(),)), ("length_ad", lambda: (AD([]),)), +]) +def test_reads_and_len(built, name, make): + differential(built.module, name, make) + + +@pytest.mark.parametrize("name,make", [ + ("get_ad", lambda: (AD(), 2)), ("get_ad", lambda: (AD(), -1)), ("get_ad", lambda: (AD(), -4)), + ("get_aq", lambda: (AQ(), 3)), ("get_aq", lambda: (AQ(), -4)), +]) +def test_index_and_negative_index(built, name, make): + differential(built.module, name, make) + + +@pytest.mark.parametrize("name,make", [ + ("get_ad", lambda: (AD(), 4)), ("get_ad", lambda: (AD(), -5)), ("get_ad", lambda: (AD([]), 0)), + ("get_aq", lambda: (AQ(), 99)), + ("set_ad", lambda: (AD(), 4, 1.0)), ("set_aq", lambda: (AQ(), -5, 1)), +]) +def test_index_errors_match_cpython_type_and_text(built, name, make): + c = differential(built.module, name, make) + assert c[0] == "raise" and c[1] is IndexError + + +@pytest.mark.parametrize("name,make", [ + ("set_ad", lambda: (AD(), 1, 9.5)), ("set_ad", lambda: (AD(), -1, 9.5)), + ("set_aq", lambda: (AQ(), 0, -77)), ("set_aq", lambda: (AQ(), 2, 2 ** 40)), + ("scale_ad", lambda: (AD(), 2.0)), ("scale_ad", lambda: (AD([]), 2.0)), + ("iota_aq", lambda: (AQ(),)), + ("copy_ad", lambda: (AD(), AD([9.0, 8.0, 7.0, 6.0]))), +]) +def test_writes_are_visible_to_the_caller(built, name, make): + differential(built.module, name, make) + + +def test_a_write_really_lands_in_the_callers_memory(built): + assert_compiled(built, "set_ad", "scale_ad") + a = AD() + built.module.set_ad(a, 1, 42.0) + assert a[1] == 42.0 + built.module.scale_ad(a, 2.0) + assert a.tolist() == [3.0, 84.0, 6.0, 9.0] + + +def test_exception_mid_loop_keeps_earlier_writes(built): + differential(built.module, "fill_n", lambda: (AD([0.0] * 3), 4)) + differential(built.module, "fill_n", lambda: (AD([0.0] * 5), 4)) + a = AD([0.0] * 3) + with pytest.raises(IndexError): + built.module.fill_n(a, 4) + assert a.tolist() == [1.0, 1.0, 1.0] + + +def test_array_array_elements_are_python_floats_division_raises(built): + differential(built.module, "div_ad", lambda: (AD([1.0, 0.0]),)) + differential(built.module, "div_ad", lambda: (AD([1.0, 4.0]),)) + + +# --- aliasing ------------------------------------------------------------------------------------- + +def test_the_same_array_passed_twice(built): + assert_compiled(built, "copy_ad", "cascade_ad") + a = AD() + before = deopts(built.module) + built.module.copy_ad(a, a) + assert deopts(built.module) == before + assert a.tolist() == F + # the same array as both operands: each read sees the program-order state + a = AD([0.0, 0.0, 0.0]) + differential(built.module, "cascade_ad", lambda: (lambda x: (x, x))(AD([1.0, 2.0, 3.0]))) + built.module.cascade_ad(a, a) + assert a.tolist() == [1.0, 1.0, 1.0] + a = AD([0.0, 0.0, 0.0]) + interp(built.module, "cascade_ad")(a, a) + assert a.tolist() == [1.0, 1.0, 1.0] + + +def test_buffer_and_list_parameters_together(built): + l = [10.0, 20.0, 30.0, 40.0] + a = AD([0.0] * 4) + before = deopts(built.module) + built.module.mixed(l, a) + assert deopts(built.module) == before + assert a.tolist() == l and l == [10.0, 20.0, 30.0, 40.0] + # an array in place of the list is a deopt (guard), not an alias + differential(built.module, "mixed", lambda: (AD([1.0] * 4), AD([0.0] * 4)), expect_deopt=True) + + +# --- guards: anything else takes the object path -------------------------------------------------- + +class SubArray(array.array): + pass + + +@pytest.mark.parametrize("name,make", [ + ("sum_aq", lambda: (AD(),)), + ("sum_aq", lambda: (array.array("Q", [1, 2]),)), + ("sum_aq", lambda: (array.array("i", [1, 2]),)), + ("sum_ad", lambda: (array.array("f", [1.0, 2.0]),)), + ("sum_ad", lambda: (SubArray("d", F),)), + ("sum_ad", lambda: (list(F),)), + ("sum_ad", lambda: (memoryview(AD()),)), + ("sum_ad", lambda: (None,)), +]) +def test_wrong_kind_takes_the_object_path_and_is_still_correct(built, name, make): + differential(built.module, name, make, expect_deopt=True) + + +def test_wrong_arity_and_keywords_still_reach_cpython(built): + assert_compiled(built, "sum_ad") + with pytest.raises(TypeError): + built.module.sum_ad() + with pytest.raises(TypeError): + built.module.sum_ad(AD(), AD()) + assert built.module.sum_ad(a=AD()) == sum(F) + + +# --- the export is released on every exit ---------------------------------------------------------- + +def can_resize(a): + try: + a.append(0.0) + except BufferError: + return False + return True + + +def test_export_released_after_ok_error_and_deopt(built): + m = built.module + a = AD() + m.sum_ad(a) + assert can_resize(a) # ok exit + a = AD() + with pytest.raises(IndexError): + m.get_ad(a, 99) + assert can_resize(a) # error exit + a = AD() + with pytest.raises(IndexError): + m.set_ad(a, 99, 1.0) + assert can_resize(a) + q = array.array("q", [2 ** 62, 2 ** 62, 2 ** 62]) + before = deopts(m) + assert m.sum_aq(q) == 3 * 2 ** 62 # overflow of i64: deopt after entry + assert deopts(m) == before + 1 + q.append(1) # released before the interpreted redo + d = array.array("d", [1.0]) + before = deopts(m) + m.sum_aq(d) # export taken, then the format check fails + assert deopts(m) == before + 1 + assert can_resize(d) + s = SubArray("d", F) + m.sum_ad(s) # type guard fails before any export + assert can_resize(s) + dst = AD() + with pytest.raises(IndexError): + m.copy_ad(dst, AD([1.0])) # the second export is held at the error + assert can_resize(dst) + dst, src = AD(), AD() + m.copy_ad(dst, src) + assert can_resize(dst) and can_resize(src) + + +def exports_leak_after_error(out, c): + mod = load_so(cbuild.build(c, "bufmod", out)) + a = AD() + with pytest.raises(IndexError): + mod.get_ad(a, 99) + return not can_resize(a) + + +def test_mutation_dropping_release_on_the_error_path_is_caught(built, tmp_path): + # a build whose error path does not call tp_buf_exit leaks the export; the same observation + # as the release test shows it, and the unmutated build does not leak + start = built.c.index("tp_error:", built.c.index("static PyObject *tp_wrap_get_ad(")) + end = built.c.index("tp_deopt:", start) + mutated = built.c[:start] + built.c[start:end].replace("tp_buf_exit(", "tp_buf_leak(") \ + + built.c[end:] + assert mutated != built.c + mutated = mutated.replace( + "static const char tp_source[]", + "static inline void tp_buf_leak(tp_buf *b) { (void)b; }\nstatic const char tp_source[]", 1) + assert exports_leak_after_error(tmp_path / "mutated", mutated) + assert not exports_leak_after_error(tmp_path / "clean", built.c) + + +def test_plain_switch_builds_and_agrees(built, tmp_path): + # -DTP_BUFFER_PLAIN: plain loads and stores instead of relaxed atomics (measurement only) + assert "TP_BUFFER_PLAIN" in (cbuild.RUNTIME_DIR / "tp_runtime.h").read_text() + so = cbuild.build(built.c, "bufmod", tmp_path / "plain", + flags=list(cbuild.DEFAULT_FLAGS) + ["-DTP_BUFFER_PLAIN"]) + m = load_so(so) + a = AD() + m.scale_ad(a, 2.0) + assert a.tolist() == [v * 2 for v in F] + assert m.sum_aq(AQ()) == sum(I) + + +def test_atomic_access_is_the_default(built): + text = (cbuild.RUNTIME_DIR / "tp_runtime.h").read_text() + assert "#ifdef TP_BUFFER_PLAIN" in text + plain, _, atomic = text.partition("#ifdef TP_BUFFER_PLAIN")[2].partition("#else") + assert "__atomic_load_n" in atomic.split("#endif")[0] and "__atomic" not in plain + # the generated code reaches the elements only through the helpers + start = built.c.index("static int tp_impl_scale_ad(", built.c.index("static const char tp_source")) + while not built.c[built.c.index(")", start) + 1:].startswith("\n{"): + start = built.c.index("static int tp_impl_scale_ad(", start + 1) + body = built.c[start:built.c.index("\n}\n", start)] + assert "tp_buf_ld_f64(" in body and "tp_buf_st_f64(" in body and "->data[" not in body + + +# --- closedness (#203) ---------------------------------------------------------------------------- + +def test_function_that_would_run_user_code_stays_interpreted(built): + assert_compiled(built, "sum_ad") + + class O: + v = 1.0 + assert built.module.open_ad(AD([2.0]), O()) == 3.0 + assert "open_ad" not in {f.name for f in built.proved.functions} + + +# --- verifier rules, on hand-built IR (the verifier never trusts the front end) ------------------- + +def test_verifier_buffer_kind_and_copy_rules(): + from test_verify import F64 as VF64, F64A as VF64A, assert_rejected, fn + from typedpython.ir import ArrayParam, Assign, Const, CopyArray, Index, Return, Type + + for kind in ("numpy", "memoryview"): + assert_rejected("verify/buffer", fn("f", params=[ArrayParam("a", VF64A, False, kind)], + returns=VF64, pure=True, + body=[Return(Index(VF64, "a", Const(Type.I64, 0)))])) + copy = fn("f", params=[ArrayParam("a", VF64A, False, "array")], returns=VF64, pure=True, + locals_={"b": VF64A}, + body=[Assign("b", CopyArray(VF64A, "a")), Return(Index(VF64, "b", Const(Type.I64, 0)))]) + assert_rejected("verify/buffer", copy) + + +def test_verifier_buffer_params_are_not_limited_but_lists_still_are(): + from test_verify import F64 as VF64, F64A as VF64A, assert_accepted, assert_rejected, fn + from typedpython.ir import ArrayParam, Const, Index, Return, Type + + def many(n, buffer): + return fn("f", params=[ArrayParam(f"a{k}", VF64A, False, buffer) for k in range(n)], + returns=VF64, pure=True, + body=[Return(Index(VF64, "a0", Const(Type.I64, 0)))]) + + assert_accepted(many(5, "array")) + assert_rejected("verify/structure", many(3, None)) diff --git a/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py b/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py new file mode 100644 index 000000000..b03d49a8c --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py @@ -0,0 +1,64 @@ +"""numpy arrays are not compiled yet (#220): an `NDArray[...]` parameter is an ordinary object +parameter, so results and their numpy scalar types equal the interpreter's. Skipped without numpy.""" +import textwrap + +import pytest + +np = pytest.importorskip("numpy") + +from typedpython import cbuild, ir # noqa: E402 +from test_buffer_arrays import AD, F, build_source, differential, load_so # noqa: E402 + +SOURCE = textwrap.dedent(''' + # typedpython: compiled + import array + import numpy as np + import numpy.typing as npt + + + def sum_ad(a: array.array[float]) -> float: + s = 0.0 + for i in range(len(a)): + s += a[i] + return s + + + def nd_first(x: npt.NDArray[np.float64]) -> object: + return x[0] + + + def nd_sum(x: npt.NDArray[np.float64]) -> object: + s = 0.0 + for i in range(len(x)): + s += x[i] + return s +''') + + +@pytest.fixture(scope="module") +def built(tmp_path_factory): + d = tmp_path_factory.mktemp("bufnp") + path, proved, diags, c = build_source(d, SOURCE) + so = cbuild.build(c, path.stem, d / "out") + return proved, load_so(so) + + +def test_numpy_parameter_is_an_ordinary_object_and_keeps_numpy_results(built): + proved, m = built + for f in proved.functions: + assert all(not isinstance(p, ir.ArrayParam) or p.buffer == "array" or p.buffer is None + for p in f.params) + assert not (f.name.startswith("nd_") and any(isinstance(p, ir.ArrayParam) for p in f.params)) + x = np.array(F, dtype=np.float64) + r = m.nd_first(x) + assert type(r) is np.float64 and r == F[0] + r = m.nd_sum(x) + ref = 0.0 + for v in x: + ref += v + assert type(r) is np.float64 and r == ref + + +def test_a_numpy_array_given_to_an_array_parameter_takes_the_object_path(built): + _, m = built + differential(m, "sum_ad", lambda: (np.array(F),), expect_deopt=True) diff --git a/python-multiplatform-ksp/src/test/python/test_cgen.py b/python-multiplatform-ksp/src/test/python/test_cgen.py index eea61452b..d7afecef8 100644 --- a/python-multiplatform-ksp/src/test/python/test_cgen.py +++ b/python-multiplatform-ksp/src/test/python/test_cgen.py @@ -89,7 +89,7 @@ def fn(name, params, returns, locals_, body, *, pure, may_deopt, line=1, entry_g # The module source that every e2e module embeds; each test picks the functions it compiles. SOURCE = textwrap.dedent('''\ - """typedpython test module — the interpreted reference.""" + """typedpython test module, the interpreted reference.""" calls = [] def record(x): @@ -713,7 +713,7 @@ def test_deopt_propagates_through_a_c_to_c_call(mod): assert deopts(mod) == before assert mod.twice_mul(7, 40) == 2 * 7 ** 40 # The outermost compiled call is redone once (+1). The interpreted twice_mul then calls the - # global mul_all — the compiled one, since there is one namespace — which deopts again (+1). + # global mul_all, the compiled one, since there is one namespace, which deopts again (+1). assert deopts(mod) == before + 2 @@ -936,14 +936,16 @@ def test_gen_accepts_proven_ops_in_an_impure_function(): proven=True))], pure=False, may_deopt=False) text = cgen.generate(Module("tp_proven_gen", (ok,)), source_file("tp_proven_gen")) - assert "typedpython: proven operation overflowed" in text + # #148: a proven op is plain arithmetic (TP_PROVEN*, tp_runtime.h), not the checked helper + assert "tp_proven(sub," in text and "tp_proven_neg(" in text + assert "tp_sub_i64(" not in text and "tp_neg_i64(" not in text def test_gen_rejects_proven_on_an_op_that_is_not_checked_i64_arithmetic(): from typedpython import cgen bad = fn("badproven", [Param("a", I64)], I64, {}, - [Return(BinOp(I64, BinOpKind.FLOORDIV, L("a", I64), c(2), proven=True))], + [Return(BinOp(I64, BinOpKind.FLOORDIV, L("a", I64), c(3), proven=True))], # 3: no shift pure=True, may_deopt=True) with pytest.raises(cgen.CGenError, match="proven"): cgen.generate(Module("tp_bad4", (bad,)), source_file("tp_bad4")) @@ -975,9 +977,24 @@ def test_proven_op_in_an_impure_function_computes_the_interpreted_result(mod): before = deopts(mod) assert mod.proven_eff(5) == interp(mod, "proven_eff")(5) == 6 assert mod.calls == [5, 5] and deopts(mod) == before + + +@pytest.fixture(scope="module") +def checked_mod(): + from typedpython import cbuild + return build_module("tp_cgen_proven_checked", (f_proven_eff,), + flags=list(cbuild.DEFAULT_FLAGS) + ["-DTP_CHECK_PROVEN"]) + + +def test_proven_op_with_a_wrong_proof_is_an_internal_error_when_proofs_are_checked(checked_mod): + # #148: a proven op is unchecked by default; -DTP_CHECK_PROVEN checks every one again. A proof + # the IR producer got wrong is then an internal error, never a deopt (the effect has happened + # already, so a redo would run it twice). + mod = checked_mod + mod.calls.clear() + before = deopts(mod) + assert mod.proven_eff(5) == 6 mod.calls.clear() - # A proof the IR producer got wrong: the overflow is an internal error, never a deopt (the - # effect has happened already, so a redo would run it twice). with pytest.raises(SystemError, match="typedpython: proven operation overflowed"): mod.proven_eff(2 ** 63 - 1) assert mod.calls == [2 ** 63 - 1] and deopts(mod) == before diff --git a/python-multiplatform-ksp/src/test/python/test_cgen_class_changes.py b/python-multiplatform-ksp/src/test/python/test_cgen_class_changes.py new file mode 100644 index 000000000..80e50c42d --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_cgen_class_changes.py @@ -0,0 +1,910 @@ +"""TypedPython C back end: a compiled fixed-layout class (SPEC N-11) changed AFTER module init. + +After init, compiled code must still compute what the interpreter computes when user code + * replaces a field's class attribute (`Node.left = property(...)`), with or without a setter; + * rebinds the module global (`Node = Twin`, `Node = Wide`, `del Node`); + * assigns `obj.__class__` to a layout-compatible class; + * changes `Node.__init__`, or adds an unrelated class attribute (`Node.counter = 1`); + * exhausts the class's version tags (tp_version_tag stays 0); +and it must do so mid-body too, when an impure compiled function runs a Python callback +(CallObject) between entry and the field access / New, without a deopt after that effect. + +Every scenario runs in a CHILD process (a crash fails one test, not the run) on a fresh compiled +module instance, and the same scenario runs on the plain interpreted source (loaded as an ordinary +module from the same file): the two normalised outcome lists must be equal. Probes read +`__typedpython_deopts__` and `__typedpython_class_info__()` of the compiled instance only. + +Builds go to `.tmp/cgen-tests/` of the worktree (test_cgen.OUT_ROOT). +""" +from __future__ import annotations + +import gc +import importlib.util +import json +import os +import subprocess +import sys +import sysconfig +import textwrap +from pathlib import Path + +import pytest + +from test_cgen import (PACKAGE, OUT_ROOT, I64, BOOL, OBJ, NONE, F64, c, L, fn, runtime_dir, + need_runtime) +from typedpython import ir +from typedpython.ir import ( + ArrayParam, Assign, BinOp, BinOpKind, Call, CallObject, ClassDecl, Compare, CompareKind, Const, + ExprStmt, FieldGet, FieldSet, If, Is, IsExact, Module, New, Param, Return, StoreIndex, Type, +) + +TEST_DIR = Path(__file__).resolve().parent + +SOURCE = textwrap.dedent('''\ + """typedpython class-change test module, the interpreted reference.""" + + class Node: + __slots__ = ("left", "right") + + def __init__(self, left, right): + self.left = left + self.right = right + + class Twin: # the same layout as Node (so __class__ can be assigned) + __slots__ = ("left", "right") + + def __init__(self, left, right): + self.left = left + self.right = right + + class Shadow: # the same layout; `left` is a property + __slots__ = ("left", "right") + + Shadow.left = property(lambda self: "shadow") + + class Wide: # a different layout (an instance __dict__) + def __init__(self, left, right): + self.left = left + self.right = right + self.extra = "wide" + + SCALE = 2.0 + + def make(depth): + if depth == 0: + return None + return Node(make(depth - 1), make(depth - 1)) + + def check(node): + if node is None: + return 0 + return 1 + check(node.left) + check(node.right) + + def pair(a, b): + return Node(a, b) + + def get_left(n): + return n.left + + def setl(n, v): + n.left = v + + def mid_get(cb, n): + cb() + return n.left + + def mid_set(cb, n, v): + cb() + n.left = v + + def mid_new(cb, a, b): + cb() + return Node(a, b) + + def arg_new(cb, b): + return Node(cb(), b) + + def via_pair(cb, a, b): + cb() + return pair(a, b) + + def is_node(cb, x): + cb() + return type(x) is Node + + def fill(n): + x = n.left + return SCALE + ''') + +NODE = ClassDecl("Node", ("left", "right"), True) +CLASSES = (NODE,) + + +def _o(name): + return L(name, OBJ) + + +def f_make(): + depth = L("depth", I64) + rec = lambda: Call(OBJ, "make", (BinOp(I64, BinOpKind.SUB, depth, c(1)),)) + return fn("make", [Param("depth", I64)], OBJ, {"a": OBJ, "b": OBJ}, + [If(Compare(BOOL, CompareKind.EQ, depth, c(0)), (Return(Const(OBJ, None)),)), + Assign("a", rec()), Assign("b", rec()), + Return(New(OBJ, "Node", (_o("a"), _o("b"))))], + pure=True, may_deopt=True) + + +def f_check(): + node = _o("node") + return fn("check", [Param("node", OBJ, cls="Node", optional=True)], I64, {"l": OBJ, "r": OBJ}, + [If(Is(BOOL, node, Const(OBJ, None)), (Return(c(0)),)), + Assign("l", FieldGet(OBJ, node, "Node", "left")), + Assign("r", FieldGet(OBJ, node, "Node", "right")), + Return(BinOp(I64, BinOpKind.ADD, + BinOp(I64, BinOpKind.ADD, c(1), Call(I64, "check", (_o("l"),))), + Call(I64, "check", (_o("r"),))))], + pure=True, may_deopt=True) + + +def f_pair(): + return fn("pair", [Param("a", OBJ), Param("b", OBJ)], OBJ, {}, + [Return(New(OBJ, "Node", (_o("a"), _o("b"))))], pure=True, may_deopt=False) + + +def f_get_left(): + return fn("get_left", [Param("n", OBJ, cls="Node")], OBJ, {}, + [Return(FieldGet(OBJ, _o("n"), "Node", "left"))], pure=True, may_deopt=False) + + +def f_setl(): + return fn("setl", [Param("n", OBJ, cls="Node"), Param("v", OBJ)], NONE, {}, + [FieldSet(_o("n"), "Node", "left", _o("v")), Return()], pure=False, may_deopt=False) + + +def _cb(): + return ExprStmt(CallObject(OBJ, _o("cb"), ())) + + +def f_mid_get(): + return fn("mid_get", [Param("cb", OBJ), Param("n", OBJ, cls="Node")], OBJ, {}, + [_cb(), Return(FieldGet(OBJ, _o("n"), "Node", "left"))], pure=False, may_deopt=False) + + +def f_mid_set(): + return fn("mid_set", [Param("cb", OBJ), Param("n", OBJ, cls="Node"), Param("v", OBJ)], NONE, {}, + [_cb(), FieldSet(_o("n"), "Node", "left", _o("v")), Return()], + pure=False, may_deopt=False) + + +def f_mid_new(): + return fn("mid_new", [Param("cb", OBJ), Param("a", OBJ), Param("b", OBJ)], OBJ, {}, + [_cb(), Return(New(OBJ, "Node", (_o("a"), _o("b"))))], pure=False, may_deopt=False) + + +def f_arg_new(): + return fn("arg_new", [Param("cb", OBJ), Param("b", OBJ)], OBJ, {}, + [Return(New(OBJ, "Node", (CallObject(OBJ, _o("cb"), ()), _o("b"))))], + pure=False, may_deopt=False) + + +def f_via_pair(): + return fn("via_pair", [Param("cb", OBJ), Param("a", OBJ), Param("b", OBJ)], OBJ, {}, + [_cb(), Return(Call(OBJ, "pair", (_o("a"), _o("b"))))], pure=False, may_deopt=False) + + +def f_is_node(): + return fn("is_node", [Param("cb", OBJ), Param("x", OBJ)], BOOL, {}, + [_cb(), Return(IsExact(BOOL, _o("x"), "Node"))], pure=False, may_deopt=False) + + +def f_fill(): + # closed (no user code) with an entry global. Not an array function any more: a slot read's + # slow path can run a property, which must not run while a list copy-in is live (#203) + return fn("fill", [Param("n", OBJ, cls="Node")], F64, + {"x": OBJ}, + [Assign("x", FieldGet(OBJ, _o("n"), "Node", "left")), + Return(L("SCALE", F64))], + pure=True, may_deopt=False, entry_globals=(Param("SCALE", F64),)) + + +def functions(): + return (f_make(), f_check(), f_pair(), f_get_left(), f_setl(), f_mid_get(), f_mid_set(), + f_mid_new(), f_arg_new(), f_via_pair(), f_is_node(), f_fill()) + + +def source_file(name: str) -> Path: + import shutil + d = OUT_ROOT / name + if d.exists(): + shutil.rmtree(d) + d.mkdir(parents=True) + p = d / f"{name}.py" + p.write_text(SOURCE) + return p + + +def generate(name, funcs=None): + from typedpython import cgen + funcs = functions() if funcs is None else funcs + return cgen.generate(Module(name=name, functions=tuple(funcs), classes=CLASSES), + source_file(name)) + + +def build(name, flags=None): + from typedpython import cbuild + need_runtime() + src = generate(name) + kwargs = {} if flags is None else {"flags": flags} + return cbuild.build(src, name, OUT_ROOT / name, runtime_dir=runtime_dir(), **kwargs) + + +# --- the child process ------------------------------------------------------------------------------- +# Everything below `child_main` runs in a separate interpreter (subprocess) so a crash or a corrupted +# class cannot leak into other tests. + +def NOOP(): + return None + + +def norm(x): + if x is None or isinstance(x, (bool, int, float, str)): + return x + if isinstance(x, (list, tuple)): + return [norm(i) for i in x] + if isinstance(x, BaseException): + return ["exc", type(x).__name__, str(x)] + d = {"type": type(x).__name__} + for f in ("left", "right", "extra"): + try: + d[f] = norm(getattr(x, f)) + except Exception as e: # noqa: BLE001, the outcome is the result + d[f] = ["exc", type(e).__name__, str(e)] + return d + + +def sc_unchanged(M, call, P): + P("before") + t = M.make(3) + n = M.Node(1, 2) + out = [norm(t), call("check", t), call("get_left", n), call("setl", n, "x"), norm(n), + call("pair", 1, 2), call("mid_get", NOOP, n), call("mid_set", NOOP, n, "y"), + call("mid_new", NOOP, 3, 4), call("arg_new", lambda: "a", 5), call("via_pair", NOOP, 6, 7), + call("is_node", NOOP, n), call("is_node", NOOP, M.Twin(1, 2))] + out += [call("fill", n)] + P("after") + return out + + +def sc_prop_get(M, call, P): + n, n2 = M.Node(1, 2), M.Node(3, 4) + P("before") + M.Node.left = property(lambda s: 42) + out = [call("get_left", n), call("mid_get", NOOP, n2), call("check", n), call("make", 1), + call("pair", 1, 2), call("mid_new", NOOP, 1, 2), call("via_pair", NOOP, 1, 2), + call("setl", n, 3), call("arg_new", NOOP, 2), call("get_left", n2)] + P("after") + return out + + +def sc_prop_set(M, call, P): + log = [] + n = M.Node(1, 2) + P("before") + M.Node.left = property(lambda s: ("got", tuple(log)), lambda s, v: log.append(v)) + out = [call("setl", n, "x"), call("mid_set", NOOP, n, "y"), call("get_left", n), list(log), + call("pair", "p", "q"), list(log), call("mid_new", NOOP, "r", "s"), list(log)] + P("after") + return out + + +def _rebind(M, cls_name, call, P): + old = M.Node(1, 2) + warm = [call("make", 1), call("pair", 0, 0), call("mid_new", NOOP, 0, 0), + call("via_pair", NOOP, 0, 0), call("arg_new", lambda: 0, 0)] # each New has looked once + P("before") + M.Node = getattr(M, cls_name) + out = warm + [call("make", 2), call("pair", 1, 2), call("mid_new", NOOP, 1, 2), + call("via_pair", NOOP, 1, 2), call("arg_new", lambda: "a", 2), + call("is_node", NOOP, M.Twin(1, 2)), call("is_node", NOOP, old), + call("check", old), call("get_left", old)] + P("after") + return out + + +def sc_rebind_twin(M, call, P): + return _rebind(M, "Twin", call, P) + + +def sc_rebind_wide(M, call, P): + return _rebind(M, "Wide", call, P) + + +def sc_rebind_back(M, call, P): + orig = M.Node + out = [call("make", 1)] + M.Node = M.Twin + out += [call("make", 1), call("pair", 1, 2)] + M.Node = orig + P("restored") + out += [call("make", 3), call("pair", 1, 2), call("mid_new", NOOP, 1, 2)] + P("after") + return out + + +def sc_del_global(M, call, P): + seen = [] + out = [call("make", 1), call("pair", 0, 0), call("arg_new", lambda: 0, 0)] + P("before") + del M.Node + out += [call("make", 1), call("pair", 1, 2), call("mid_new", NOOP, 1, 2), + call("arg_new", lambda: seen.append("cb") or "a", 2), list(seen), + call("via_pair", NOOP, 1, 2), call("is_node", NOOP, 5)] + P("after") + return out + + +def sc_mid_prop(M, call, P): + n = M.Node(1, 2) + out = [call("mid_get", NOOP, n)] # warm: the fast path + P("before") + + def cb(): + M.Node.left = property(lambda s: 42) + out += [call("mid_get", cb, n)] + P("after") + return out + + +def sc_mid_prop_set(M, call, P): + log = [] + n = M.Node(1, 2) + out = [call("mid_set", NOOP, n, 0)] + P("before") + + def cb(): + M.Node.left = property(lambda s: "g", lambda s, v: log.append(v)) + out += [call("mid_set", cb, n, "v"), list(log)] + P("after") + return out + + +def sc_mid_rebind(M, call, P): + out = [call("mid_new", NOOP, 0, 0)] + P("before") + out += [call("mid_new", lambda: setattr(M, "Node", M.Twin), 1, 2)] + P("after") + return out + + +def sc_arg_rebind(M, call, P): + out = [call("arg_new", lambda: 0, 0)] + P("before") + + def cb(): + M.Node = M.Twin + return "a" + out += [call("arg_new", cb, 2), call("arg_new", lambda: "b", 3)] # the 2nd builds a Twin + P("after") + return out + + +def sc_mid_class(M, call, P): + n, n2 = M.Node(1, 2), M.Node(3, 4) + out = [call("mid_get", NOOP, n)] + P("before") + + def to_shadow(): + n.__class__ = M.Shadow + + def to_twin(): + n2.__class__ = M.Twin + out += [call("mid_get", to_shadow, n), call("mid_set", to_twin, n2, 9), norm(n2)] + P("after") + out += [call("mid_set", NOOP, n, 7)] # guard: n is a Shadow now → interpreted + return out + + +def sc_mid_is_node(M, call, P): + out = [call("is_node", NOOP, M.Node(1, 2))] + P("before") + out += [call("is_node", lambda: setattr(M, "Node", M.Twin), M.Twin(1, 2))] + P("after") + return out + + +def sc_mid_init(M, call, P): + def swapped(self, l, r): + M.Node.left.__set__(self, r) + M.Node.right.__set__(self, l) + init = swapped + + def cb(): + M.Node.__init__ = init + out = [call("mid_new", NOOP, 0, 0), call("via_pair", NOOP, 0, 0)] + P("before") + out += [call("mid_new", cb, 1, 2)] + P("mid") + out += [call("via_pair", NOOP, 3, 4), call("pair", 5, 6), call("make", 1)] + P("after") + return out + + +def sc_counter(M, call, P): + t0 = M.make(2) + P("before") + M.Node.counter = 1 + n = M.Node(1, 2) + out = [call("make", 3), call("check", t0), call("get_left", n), call("setl", n, "x"), norm(n), + call("pair", 1, 2), call("mid_get", NOOP, n), call("mid_new", NOOP, 1, 2)] + P("after") + return out + + +def sc_exhaust(M, call, P): + n = M.Node(1, 2) + for i in range(1100): # MAX_VERSIONS_PER_CLASS is 1000 in CPython 3.14 + M.Node.x = i + M.Node.x # a lookup assigns a fresh tag (until exhausted) + P("before") + out = [call("make", 2), call("check", M.Node(None, None)), call("get_left", n), + call("setl", n, "s"), call("mid_get", NOOP, n), call("mid_set", NOOP, n, "t"), + call("pair", 1, 2), call("via_pair", NOOP, 1, 2), call("mid_new", NOOP, 1, 2)] + P("after") + return out + + +def sc_entry_global(M, call, P): + n = M.Node(1, 2) + out = [call("fill", n)] + P("before") + + def getter(s): + M.SCALE = 5.0 + return "p" + M.Node.left = property(getter) + out += [call("fill", n), M.SCALE] + P("after") + return out + + +SCENARIOS = {k[3:]: v for k, v in dict(globals()).items() if k.startswith("sc_")} + + +def _exhaust_dict_watchers(): + """Take every free dict-watcher id of this interpreter (ctypes); return them.""" + import ctypes + CB = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p) + add = ctypes.pythonapi.PyDict_AddWatcher + add.argtypes, add.restype = [CB], ctypes.c_int + keep, ids = [], [] + while True: + cb = CB(lambda *a: 0) + keep.append(cb) + try: + ids.append(add(cb)) + except RuntimeError: + break + if len(ids) > 16: + raise AssertionError(f"PyDict_AddWatcher never ran out: {ids}") + _exhaust_dict_watchers.keep = keep + return ids + + +def _clear_dict_watchers(ids): + import ctypes + clear = ctypes.pythonapi.PyDict_ClearWatcher + clear.argtypes, clear.restype = [ctypes.c_int], ctypes.c_int + for i in ids: + clear(i) + + +_counter = [0] + + +def _load_reference(src): + _counter[0] += 1 + spec = importlib.util.spec_from_file_location(f"tp_ref_{_counter[0]}", src) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def _run(M, scenario, compiled): + probes = [] + + def P(label): + if not compiled: + return + info = getattr(M, "__typedpython_class_info__", None) + probes.append({"label": label, "deopts": M.__typedpython_deopts__, + "info": info() if info is not None else None}) + + def call(name, *args): + try: + return norm(getattr(M, name)(*args)) + except Exception as e: # noqa: BLE001 + return ["exc", type(e).__name__, str(e)] + try: + out = SCENARIOS[scenario](M, call, P) + except Exception as e: # noqa: BLE001, a scenario must not raise itself + out = ["scenario-raised", type(e).__name__, str(e)] + return out, probes + + +def child_main(so, src, name, scenarios, exhaust=False): + from typedpython import cbuild + report = {"scenarios": {}} + if exhaust: + report["taken_watcher_ids"] = _exhaust_dict_watchers() + for sc in scenarios: + gc.collect() # a dropped instance releases its watcher id + A = cbuild.load(name, so) + R = _load_reference(src) + got, probes = _run(A, sc, True) + want, _ = _run(R, sc, False) + report["scenarios"][sc] = {"compiled": got, "reference": want, "probes": probes} + del A, R + print("REPORT " + json.dumps(report)) + + +def child_instances(so, name, n, keep_alive): + """Load `n` instances; report what each says about its dict watcher.""" + from typedpython import cbuild + kept, out = [], [] + for _ in range(n): + if not keep_alive: + gc.collect() + A = cbuild.load(name, so) + info = getattr(A, "__typedpython_class_info__", None) + watched = info()["Node"]["watched"] if info else None + # correctness on every instance, watched or not: a rebinding seen by New + A.make(1) + A.Node = A.Twin + out.append([watched, type(A.make(1)).__name__, type(A.pair(1, 2)).__name__]) + if keep_alive: + kept.append(A) + del A + print("REPORT " + json.dumps(out)) + + +def child_refcounts(so, name, rounds): + from typedpython import cbuild + shared = object() + A1 = cbuild.load(name, so) # unchanged: fast paths + A2 = cbuild.load(name, so) # a property with a setter: slow / deopt paths + A3 = cbuild.load(name, so) # the global rebound: New slow / deopt paths + A4 = cbuild.load(name, so) # tags exhausted: every check fails + store = [] + A2.Node.left = property(lambda s: shared, lambda s, v: (store.append(v), store.pop())) + n1, n2, n4 = A1.Node(shared, shared), A2.Node.__new__(A2.Node), A4.Node(shared, shared) + A2.Node.right.__set__(n2, shared) + A3.make(1) + A3.Node = A3.Twin + for i in range(1100): + A4.Node.x = i + A4.Node.x + cb = lambda: shared # noqa: E731 + + def round_(): + for M, n in ((A1, n1), (A4, n4)): + t = M.make(3) + M.check(t) + M.get_left(n) + M.setl(n, shared) + M.pair(shared, shared) + M.mid_get(cb, n) + M.mid_set(cb, n, shared) + M.mid_new(cb, shared, shared) + M.via_pair(cb, shared, shared) + M.arg_new(cb, shared) + M.is_node(cb, n) + M.fill(n) + A2.get_left(n2) + A2.mid_get(cb, n2) + A2.setl(n2, shared) + A2.mid_set(cb, n2, shared) + A2.fill(n2) + try: + A2.check(n2) + except AttributeError: + pass + try: + A2.pair(shared, shared) + except AttributeError: + pass + A3.make(2) + A3.pair(shared, shared) + A3.mid_new(cb, shared, shared) + A3.via_pair(cb, shared, shared) + A3.arg_new(cb, shared) + A3.is_node(cb, n1) + + for _ in range(30): + round_() + objs = [shared, n1, n2, n4, A1.Node, A2.Node, A3.Node, A3.Twin, A4.Node, cb] + base = [sys.getrefcount(o) for o in objs] + d0 = [M.__typedpython_deopts__ for M in (A1, A2, A3, A4)] + for _ in range(rounds): + round_() + after = [sys.getrefcount(o) for o in objs] + d1 = [M.__typedpython_deopts__ for M in (A1, A2, A3, A4)] + print("REPORT " + json.dumps({"base": base, "after": after, "deopts": [d0, d1]})) + + +def _child_cmd(call: str): + return [sys.executable, "-c", + f"import sys; sys.path[:0] = [{str(TEST_DIR)!r}, {str(PACKAGE.parent)!r}]; " + f"import test_cgen_class_changes as T; T.{call}"] + + +def _report(run): + for line in run.stdout.splitlines(): + if line.startswith("REPORT "): + return json.loads(line[len("REPORT "):]) + raise AssertionError(f"child failed (rc={run.returncode}):\n{run.stdout[-4000:]}\n" + f"{run.stderr[-6000:]}") + + +def run_scenarios(so, name, scenarios, exhaust=False, launcher=None, env=None): + src = OUT_ROOT / name / f"{name}.py" + cmd = _child_cmd(f"child_main({str(so)!r}, {str(src)!r}, {name!r}, {list(scenarios)!r}, " + f"exhaust={exhaust!r})") + if launcher is not None: + cmd[0] = str(launcher) + run = subprocess.run(cmd, capture_output=True, text=True, env=env) + rep = _report(run) + assert run.returncode == 0, run.stderr[-4000:] + return rep, run + + +# --- fixtures ----------------------------------------------------------------------------------------- + +NAME = "tp_cls_chg" + + +@pytest.fixture(scope="module") +def so(): + return build(NAME) + + +def same(rep, sc): + r = rep["scenarios"][sc] + assert r["compiled"] == r["reference"], json.dumps(r, indent=1) + return r + + +def probe(r, label): + for p in r["probes"]: + if p["label"] == label: + return p + raise AssertionError(f"no probe {label}: {r['probes']}") + + +# --- the IR used here is what the verifier accepts -------------------------------------------------- + +def test_the_hand_built_ir_passes_the_verifier(): + from typedpython import verify + proved, diags = verify.verify(Module(name="m", functions=functions(), classes=CLASSES)) + errors = [d for d in diags if getattr(d, "severity", "error") == "error"] + assert not proved.skipped, (proved.skipped, diags) + assert {f.name for f in proved.functions} == {f.name for f in functions()}, errors + + +# --- behaviour: watched module dict (the normal path) ------------------------------------------------ + +ALL_SCENARIOS = ("unchanged", "prop_get", "prop_set", "rebind_twin", "rebind_wide", "rebind_back", + "del_global", "mid_prop", "mid_prop_set", "mid_rebind", "arg_rebind", "mid_class", + "mid_is_node", "mid_init", "counter", "exhaust", "entry_global") + + +@pytest.fixture(scope="module") +def report(so): + rep, _ = run_scenarios(so, NAME, ALL_SCENARIOS) + return rep + + +@pytest.mark.parametrize("sc", ALL_SCENARIOS) +def test_compiled_equals_interpreted_after_the_class_changes(report, sc): + r = same(report, sc) + for p in r["probes"]: + assert p["info"] is not None and p["info"]["Node"]["watched"] is True, p # watcher path + + +def test_unchanged_class_stays_on_the_fast_path(report): + r = same(report, "unchanged") + b, a = probe(r, "before"), probe(r, "after") + assert a["deopts"] == b["deopts"] + assert a["info"]["Node"]["slow"] == b["info"]["Node"]["slow"] == 0 + assert a["info"]["Node"]["current"] is True + + +@pytest.mark.parametrize("sc", ("mid_prop", "mid_prop_set", "mid_rebind", "arg_rebind", "mid_class", + "mid_is_node")) +def test_impure_function_takes_the_slow_path_mid_body_without_a_deopt(report, sc): + r = same(report, sc) + b, a = probe(r, "before"), probe(r, "after") + assert a["deopts"] == b["deopts"], (b, a) # no deopt after the effect + if sc != "arg_rebind": # arg_rebind: New was loaded before cb + assert a["info"]["Node"]["slow"] > b["info"]["Node"]["slow"], (b, a) + + +def test_property_reads_give_the_interpreted_value(report): + r = same(report, "prop_get") + assert r["compiled"][0] == 42 and r["compiled"][1] == 42 + + +def test_property_setter_runs_on_field_set(report): + r = same(report, "prop_set") + assert r["compiled"][3] == ["x", "y"] + + +def test_rebound_global_builds_the_new_class(report): + for sc, cls in (("rebind_twin", "Twin"), ("rebind_wide", "Wide")): + r = same(report, sc) + made = r["compiled"][5:10] + assert all(isinstance(x, dict) and x["type"] == cls for x in made[1:4]), made + + +def test_rebinding_back_returns_to_the_fast_path(report): + r = same(report, "rebind_back") + assert probe(r, "after")["deopts"] == probe(r, "restored")["deopts"] + + +def test_mid_body_init_change_and_class_redo_of_a_pure_callee(report): + r = same(report, "mid_init") + assert r["compiled"][2] == {"type": "Node", "left": 2, "right": 1, "extra": r["compiled"][2]["extra"]} + # mid_new (impure) took the slow path, no deopt; via_pair's pure callee `pair` was redone + assert probe(r, "mid")["deopts"] == probe(r, "before")["deopts"] + + +def test_unrelated_class_attribute_refreshes_the_tag_back_to_the_fast_path(report): + r = same(report, "counter") + b, a = probe(r, "before"), probe(r, "after") + assert a["deopts"] == b["deopts"], (b, a) + assert a["info"]["Node"]["slow"] == 0, a + assert a["info"]["Node"]["refreshed"] >= 1 and a["info"]["Node"]["current"] is True, a + assert a["info"]["Node"]["captured_tag"] != b["info"]["Node"]["captured_tag"], (b, a) + + +def test_exhausted_version_tags_take_the_slow_path(report): + r = same(report, "exhaust") + b, a = probe(r, "before"), probe(r, "after") + assert b["info"]["Node"]["tag"] == 0 and a["info"]["Node"]["tag"] == 0, (b, a) + assert a["info"]["Node"]["current"] is False + assert a["info"]["Node"]["slow"] > b["info"]["Node"]["slow"] + assert a["deopts"] > b["deopts"] # the pure functions deopted + + +def test_closed_function_with_an_entry_global_sees_the_rebinding(report): + r = same(report, "entry_global") + # fill returns SCALE: the old value before, the value the property getter set after + assert r["compiled"][0] == 2.0 and r["compiled"][1] == 5.0 + + +# --- behaviour: no dict watcher available ------------------------------------------------------------ + +FALLBACK = ("rebind_twin", "rebind_wide", "rebind_back", "del_global", "mid_rebind", "arg_rebind", + "mid_is_node", "unchanged") + + +def test_without_a_dict_watcher_rebinding_is_still_seen(so): + rep, run = run_scenarios(so, NAME, FALLBACK, exhaust=True) + assert 1 <= len(rep["taken_watcher_ids"]) <= 6, rep["taken_watcher_ids"] + for sc in FALLBACK: + r = same(rep, sc) + assert r["probes"], sc + for p in r["probes"]: + assert p["info"]["Node"]["watched"] is False, p # the conservative path ran + r = rep["scenarios"]["mid_rebind"] + assert probe(r, "after")["deopts"] == probe(r, "before")["deopts"] + + +def test_each_instance_gets_and_releases_its_own_watcher(so): + run = subprocess.run(_child_cmd(f"child_instances({str(so)!r}, {NAME!r}, 12, False)"), + capture_output=True, text=True) + rows = _report(run) + assert all(w is True for w, _, _ in rows), rows # released on module free + assert all(m == "Twin" and p == "Twin" for _, m, p in rows), rows + run = subprocess.run(_child_cmd(f"child_instances({str(so)!r}, {NAME!r}, 9, True)"), + capture_output=True, text=True) + rows = _report(run) + assert rows[0][0] is True and rows[-1][0] is False, rows # ids run out: fallback + assert all(m == "Twin" and p == "Twin" for _, m, p in rows), rows + + +# --- reference counts ------------------------------------------------------------------------------ + +def test_reference_counts_are_flat_over_fast_and_slow_paths(so): + run = subprocess.run(_child_cmd(f"child_refcounts({str(so)!r}, {NAME!r}, 1500)"), + capture_output=True, text=True) + rep = _report(run) + assert rep["base"] == rep["after"], rep + d0, d1 = rep["deopts"] + assert d1[0] == d0[0] and d1[1] > d0[1] and d1[2] > d0[2] and d1[3] > d0[3], rep + + +# --- generation contract ------------------------------------------------------------------------------- + +def test_gen_uses_only_runtime_helpers(): + text = generate("tp_cls_chg_gen") + for h in ("tp_class_capture", "tp_class_current", "tp_class_global_ok", "tp_getattr", + "tp_setattr", "tp_globals_watch_event", "tp_globals_unwatch"): + assert h in text, h + assert "PyMember_" not in text and "tp_version_tag" not in text + assert "PyDict_Watch(" not in text and "PyDict_AddWatcher" not in text + + +# --- mutations --------------------------------------------------------------------------------------- + +def _mutant(monkeypatch, name, attr, value): + from typedpython import cgen, cbuild + monkeypatch.setattr(cgen, attr, value) + src = generate(name) + monkeypatch.undo() + return cbuild.build(src, name, OUT_ROOT / name, runtime_dir=runtime_dir()) + + +def test_mutation_check_always_current_fails_the_property_test(monkeypatch): + need_runtime() + so = _mutant(monkeypatch, "tp_cls_chg_mut1", "_class_current_expr", lambda *a, **k: "1") + rep, _ = run_scenarios(so, "tp_cls_chg_mut1", ("prop_get",)) + r = rep["scenarios"]["prop_get"] + assert r["compiled"] != r["reference"], "the mutant (no per-access check) passed prop_get" + + +def test_mutation_watcher_never_fires_fails_the_rebinding_test(monkeypatch): + need_runtime() + so = _mutant(monkeypatch, "tp_cls_chg_mut2", "_watch_event_stmt", + lambda: "(void)event; (void)key; return 0;") + rep, _ = run_scenarios(so, "tp_cls_chg_mut2", ("rebind_twin",)) + r = rep["scenarios"]["rebind_twin"] + assert r["compiled"] != r["reference"], "the mutant (silent watcher) passed rebind_twin" + + +# --- sanitizers ----------------------------------------------------------------------------------------- + +def test_class_change_scenarios_under_address_and_undefined_sanitizers(): + need_runtime() + cc = sysconfig.get_config_var("CC") or "cc" + if sys.platform != "darwin": + pytest.skip("sanitizer preload recipe written for macOS only") + probe_rt = subprocess.run(cc.split() + ["-print-file-name=libclang_rt.asan_osx_dynamic.dylib"], + capture_output=True, text=True) + if probe_rt.returncode != 0 or not Path(probe_rt.stdout.strip()).is_file(): + pytest.skip(f"host compiler {cc!r} has no ASan runtime ({probe_rt.stdout.strip()!r})") + from typedpython import cbuild + name = "tp_cls_chg_asan" + flags = list(cbuild.DEFAULT_FLAGS) + ["-O1", "-g", "-fno-omit-frame-pointer", + "-fsanitize=address,undefined", + "-fno-sanitize-recover=undefined"] + so = build(name, flags=flags) + d = so.parent + launcher_c, launcher = d / "asan_python.c", d / "asan_python" + launcher_c.write_text("#include \nint main(int argc, char **argv) " + "{ return Py_BytesMain(argc, argv); }\n") + link = subprocess.run(cc.split() + ["-fsanitize=address,undefined", "-g", + f"-I{sysconfig.get_paths()['include']}", str(launcher_c), + f"-L{sysconfig.get_config_var('LIBDIR')}", + f"-lpython{sysconfig.get_config_var('LDVERSION')}", + "-o", str(launcher)], capture_output=True, text=True) + if link.returncode != 0: + pytest.skip(f"cannot link a sanitized launcher: {link.stderr[-2000:]}") + env = dict(os.environ, PYTHONHOME=sys.base_prefix, + PYTHONPATH=os.pathsep.join(p for p in sys.path if p and "site-packages" in p), + ASAN_OPTIONS="detect_leaks=0:abort_on_error=0:halt_on_error=1", + UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1") + env.pop("DYLD_INSERT_LIBRARIES", None) + run = subprocess.run([str(launcher)] + _child_cmd( + f"child_main({str(so)!r}, {str(so.parent / (name + '.py'))!r}, {name!r}, " + f"{list(ALL_SCENARIOS)!r})")[1:], capture_output=True, text=True, env=env) + out = run.stdout + run.stderr + if "violates platform policy" in out or "Interceptors are not working" in out: + pytest.skip(f"ASan runtime could not be loaded: {out[-2000:]}") + assert run.returncode == 0, out[-6000:] + rep = _report(run) + for sc in ALL_SCENARIOS: + same(rep, sc) + linked = subprocess.run(["otool", "-L", str(so)], capture_output=True, text=True).stdout + assert "libclang_rt.asan" in linked, linked + assert "runtime error" not in out and "AddressSanitizer" not in out, out[-6000:] diff --git a/python-multiplatform-ksp/src/test/python/test_cgen_classes.py b/python-multiplatform-ksp/src/test/python/test_cgen_classes.py new file mode 100644 index 000000000..dfbeb0058 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_cgen_classes.py @@ -0,0 +1,605 @@ +"""TypedPython C back end: fixed-layout classes (SPEC N-11; ir.ClassDecl / FieldGet / FieldSet / New / +IsExact / CheckExact / Is / Param.cls). + +IR is built by hand; every end-to-end test compares the compiled function with the interpreted one the +extension keeps in `__typedpython_interpreted__` (INTENT §1.4). Builds go to `.tmp/cgen-tests/` of the +worktree, inside the caller's heavy.sh lock (see test_cgen.py). + +Ordering of the red/green record: this file was written before the back end supported these nodes; +before the implementation every end-to-end test failed with `CGenError: no lowering for ...`. +""" +from __future__ import annotations + +import subprocess +import sys +import sysconfig +import textwrap +import os +from pathlib import Path + +import pytest + +import test_cgen as tc +from test_cgen import (PACKAGE, OUT_ROOT, I64, BOOL, OBJ, NONE, c, L, fn, runtime_dir, need_runtime) +from typedpython import ir +from typedpython.ir import ( + Assign, BinOp, BinOpKind, Call, CheckExact, ClassDecl, Compare, CompareKind, Const, FieldGet, + FieldSet, If, Is, IsExact, Module, New, Param, Return, +) + +SOURCE = textwrap.dedent('''\ + """typedpython class test module, the interpreted reference.""" + + class Node: + __slots__ = ("left", "right") + + def __init__(self, left, right): + self.left = left + self.right = right + + class Sub(Node): # a subclass whose `left` is NOT the slot + __slots__ = () + + @property + def left(self): + return None + + def mk_sub(l, r): + s = Sub.__new__(Sub) + Node.left.__set__(s, l) + Node.right.__set__(s, r) + return s + + class Fat: # no __slots__: instances have a __dict__ + def __init__(self, left, right): + self.left = left + self.right = right + + class Prop: # a property shadows a field after creation + __slots__ = ("left", "right") + + Prop.left = property(lambda self: "shadowed") + + class Other: + __slots__ = ("left", "right") + + class Stolen: # a member descriptor of ANOTHER class under our name + __slots__ = ("left", "right") + + def __init__(self, left, right): + pass + + Stolen.left = Other.left + + def make(depth): + if depth == 0: + return None + return Node(make(depth - 1), make(depth - 1)) + + def check(node): + if node is None: + return 0 + return 1 + check(node.left) + check(node.right) + + def setl(node, v): + node.left = v + + def left_checked(x): + return x.left + + def is_node(x): + return type(x) is Node + + def is_not_none(x): + return x is not None + + def same(a, b): + return a is b + + def ret_none(): + return None + + def mkfat(depth): + if depth == 0: + return None + return Fat(mkfat(depth - 1), mkfat(depth - 1)) + + def check_fat(node): + if node is None: + return 0 + return 1 + check_fat(node.left) + check_fat(node.right) + + def left_of_prop(p): + return p.left + + def left_of_stolen(p): + return p.left + ''') + +NODE = ClassDecl("Node", ("left", "right"), True) +FAT = ClassDecl("Fat", ("left", "right"), True) +PROP = ClassDecl("Prop", ("left", "right"), True) +STOLEN = ClassDecl("Stolen", ("left", "right"), True) +CLASSES = (NODE, FAT, PROP, STOLEN) + + +def f_make(name="make", cls="Node"): + depth, a, b = L("depth", I64), L("a", OBJ), L("b", OBJ) + rec = lambda: Call(OBJ, name, (BinOp(I64, BinOpKind.SUB, depth, c(1)),)) + return fn(name, [Param("depth", I64)], OBJ, {"a": OBJ, "b": OBJ}, + [If(Compare(BOOL, CompareKind.EQ, depth, c(0)), (Return(Const(OBJ, None)),)), + Assign("a", rec()), Assign("b", rec()), + Return(New(OBJ, cls, (a, b)))], + pure=True, may_deopt=True) + + +def f_check(name="check", cls="Node"): + node, l, r = L("node", OBJ), L("l", OBJ), L("r", OBJ) + ca = Call(I64, name, (l,)) + cb = Call(I64, name, (r,)) + return fn(name, [Param("node", OBJ, cls=cls, optional=True)], I64, {"l": OBJ, "r": OBJ}, + [If(Is(BOOL, node, Const(OBJ, None)), (Return(c(0)),)), + Assign("l", FieldGet(OBJ, node, cls, "left")), + Assign("r", FieldGet(OBJ, node, cls, "right")), + Return(BinOp(I64, BinOpKind.ADD, BinOp(I64, BinOpKind.ADD, c(1), ca), cb))], + pure=True, may_deopt=True) + + +def f_setl(): + node, v = L("node", OBJ), L("v", OBJ) + return fn("setl", [Param("node", OBJ, cls="Node"), Param("v", OBJ)], NONE, {}, + [FieldSet(node, "Node", "left", v), Return()], + pure=False, may_deopt=False) + + +def f_left_checked(): + x = L("x", OBJ) + return fn("left_checked", [Param("x", OBJ)], OBJ, {}, + [Return(FieldGet(OBJ, CheckExact(OBJ, x, "Node"), "Node", "left"))], + pure=True, may_deopt=True) + + +def f_is_node(): + return fn("is_node", [Param("x", OBJ)], BOOL, {}, [Return(IsExact(BOOL, L("x", OBJ), "Node"))], + pure=True, may_deopt=False) + + +def f_is_not_none(): + return fn("is_not_none", [Param("x", OBJ)], BOOL, {}, + [Return(Is(BOOL, L("x", OBJ), Const(OBJ, None), negate=True))], + pure=True, may_deopt=False) + + +def f_same(): + return fn("same", [Param("a", OBJ), Param("b", OBJ)], BOOL, {}, + [Return(Is(BOOL, L("a", OBJ), L("b", OBJ)))], pure=True, may_deopt=False) + + +def f_ret_none(): + return fn("ret_none", [], OBJ, {}, [Return(Const(OBJ, None))], pure=True, may_deopt=False) + + +def f_left_of(name, cls): + return fn(name, [Param("p", OBJ, cls=cls)], OBJ, {}, + [Return(FieldGet(OBJ, L("p", OBJ), cls, "left"))], pure=True, may_deopt=True) + + +def functions(): + return (f_make(), f_check(), f_setl(), f_left_checked(), f_is_node(), f_is_not_none(), f_same(), + f_ret_none(), f_make("mkfat", "Fat"), f_check("check_fat", "Fat"), + f_left_of("left_of_prop", "Prop"), f_left_of("left_of_stolen", "Stolen")) + + +def source_file(name: str) -> Path: + import shutil + d = OUT_ROOT / name + if d.exists(): + shutil.rmtree(d) + d.mkdir(parents=True) + p = d / f"{name}.py" + p.write_text(SOURCE) + return p + + +def generate(name, funcs=None, classes=CLASSES): + from typedpython import cgen + funcs = functions() if funcs is None else funcs + return cgen.generate(Module(name=name, functions=tuple(funcs), classes=tuple(classes)), + source_file(name)) + + +def build(name, flags=None, funcs=None, classes=CLASSES): + from typedpython import cbuild + need_runtime() + src = generate(name, funcs, classes) + kwargs = {} if flags is None else {"flags": flags} + so = cbuild.build(src, name, OUT_ROOT / name, runtime_dir=runtime_dir(), **kwargs) + return cbuild.load(name, so), so + + +@pytest.fixture(scope="module") +def m(): + return build("tp_cgen_cls")[0] + + +def interp(m, name): + return m.__typedpython_interpreted__[name] + + +def tree_size(n): + return 0 if n is None else 1 + tree_size(n.left) + tree_size(n.right) + + +# --- behaviour --------------------------------------------------------------------------------------- + +def test_make_and_check_match_the_interpreted_results(m): + for depth in (0, 1, 2, 5, 10): + before = m.__typedpython_deopts__ + tree = m.make(depth) + assert type(tree) is m.Node or tree is None + assert tree_size(tree) == 2 ** depth - 1 + assert m.check(tree) == interp(m, "check")(tree) == 2 ** depth - 1 + assert m.__typedpython_deopts__ == before # compiled all the way + + +def test_compiled_objects_are_ordinary_instances(m): + t = m.make(2) + assert type(t) is m.Node and not hasattr(t, "__dict__") + assert t.left.left is None and t.left.right is None + assert interp(m, "check")(t) == 3 + + +def test_unset_slot_raises_cpythons_attribute_error(m): + bare = m.Node.__new__(m.Node) + with pytest.raises(AttributeError) as want: + interp(m, "check")(bare) + with pytest.raises(AttributeError) as got: + m.check(bare) + assert str(got.value) == str(want.value) + assert str(got.value).endswith("Node' object has no attribute 'left'") + half = m.Node.__new__(m.Node) + m.Node.left.__set__(half, None) + with pytest.raises(AttributeError) as got2: + m.check(half) + assert str(got2.value).endswith("Node' object has no attribute 'right'") + + +def test_subclass_instance_takes_the_interpreted_path(m): + sub = m.mk_sub(m.Node(None, None), None) + assert interp(m, "check")(sub) == 1 # the property makes `left` None + before = m.__typedpython_deopts__ + assert m.check(sub) == 1 # raw slot would give 2 + assert m.__typedpython_deopts__ == before + 1 + + +def test_nested_subclass_instance_deopts_the_whole_call(m): + sub = m.mk_sub(m.Node(None, None), None) + tree = m.Node(sub, m.Node(None, None)) + want = interp(m, "check")(tree) + before = m.__typedpython_deopts__ + assert m.check(tree) == want == 3 + # the outermost call is redone interpreted; the interpreted body then calls the (compiled) global + # `check` on each child, so the subclass child deopts again: at least one, never zero + assert m.__typedpython_deopts__ >= before + 1 + + +def test_other_argument_types_take_the_interpreted_path(m): + def outcome(f, x): + try: + return ("ok", f(x)) + except Exception as e: # noqa: BLE001, the type and text are the result + return (type(e), str(e)) + + for bad in (5, "x", object(), m.Fat(None, None), m.Fat(m.Fat(None, None), None)): + want = outcome(interp(m, "check"), bad) + before = m.__typedpython_deopts__ + assert outcome(m.check, bad) == want + assert m.__typedpython_deopts__ >= before + 1 + + +def test_field_set_is_visible_to_python(m): + n = m.Node(1, 2) + assert m.setl(n, "x") is None + assert n.left == "x" and n.right == 2 + assert m.check(m.Node(None, None)) == 1 + keep = object() + m.setl(n, keep) + assert n.left is keep + + +def test_field_set_on_a_subclass_goes_interpreted(m): + sub = m.mk_sub(None, None) + before = m.__typedpython_deopts__ + with pytest.raises(AttributeError) as want: + interp(m, "setl")(sub, 1) + with pytest.raises(AttributeError) as got: + m.setl(sub, 1) + assert str(got.value) == str(want.value) + assert m.__typedpython_deopts__ == before + 1 + + +def test_check_exact_deopts_on_mismatch_and_reads_otherwise(m): + n = m.Node("l", "r") + before = m.__typedpython_deopts__ + assert m.left_checked(n) == "l" + assert m.__typedpython_deopts__ == before + sub = m.mk_sub("l", "r") + assert m.left_checked(sub) is interp(m, "left_checked")(sub) is None + assert m.__typedpython_deopts__ == before + 1 + with pytest.raises(AttributeError): + m.left_checked(5) + assert m.__typedpython_deopts__ == before + 2 + + +def test_is_exact_is_and_none_constants(m): + assert m.is_node(m.Node(1, 2)) is True + assert m.is_node(m.mk_sub(1, 2)) is False + assert m.is_node(None) is False and m.is_node(5) is False + x = object() + assert m.same(x, x) is True and m.same(x, object()) is False + assert m.same(None, None) is True + assert m.is_not_none(None) is False and m.is_not_none(0) is True + assert m.ret_none() is None + + +# --- classes that are not compiled --------------------------------------------------------------------- + +def test_class_with_a_dict_is_not_compiled_and_nothing_crashes(m): + before = m.__typedpython_deopts__ + tree = m.mkfat(4) # New of a class without __slots__ + assert type(tree) is m.Fat and tree.left is not None + assert m.check_fat(tree) == 15 == interp(m, "check_fat")(tree) + assert m.__typedpython_deopts__ > before # the calls took the interpreted path + assert m.check_fat(None) == 0 + + +def test_property_shadowing_a_field_is_not_compiled(m): + p = m.Prop() + before = m.__typedpython_deopts__ + assert m.left_of_prop(p) == "shadowed" == interp(m, "left_of_prop")(p) + assert m.__typedpython_deopts__ == before + 1 + + +def test_descriptor_of_another_class_is_not_compiled(m): + s = m.Stolen.__new__(m.Stolen) + before = m.__typedpython_deopts__ + with pytest.raises(TypeError) as want: + interp(m, "left_of_stolen")(s) + with pytest.raises(TypeError) as got: + m.left_of_stolen(s) + assert str(got.value) == str(want.value) + assert m.__typedpython_deopts__ == before + 1 + + +def test_missing_class_is_not_compiled(tmp_path): + mod, _ = build("tp_cgen_cls_missing", funcs=(f_make(),), classes=(ClassDecl("Nope", ("a",), True), + NODE)) + before = mod.__typedpython_deopts__ + assert type(mod.make(2)) is mod.Node # Node is fine; the missing one is unused + assert mod.__typedpython_deopts__ == before + + +# --- generation contract ------------------------------------------------------------------------------- + +def test_gen_rejects_check_exact_in_an_impure_function(): + bad2 = fn("bad2", [Param("x", OBJ)], OBJ, {}, + [Return(CheckExact(OBJ, L("x", OBJ), "Node"))], pure=False, may_deopt=False) + from typedpython import cgen + with pytest.raises(cgen.CGenError, match="CheckExact"): + generate("tp_gen_cls_bad", funcs=(bad2,)) + + +def test_gen_rejects_field_set_in_a_pure_function(): + bad = fn("bad", [Param("x", OBJ, cls="Node")], NONE, {}, + [FieldSet(L("x", OBJ), "Node", "left", Const(OBJ, None)), Return()], + pure=True, may_deopt=False) + from typedpython import cgen + with pytest.raises(cgen.CGenError, match="pure"): + generate("tp_gen_cls_bad2", funcs=(bad,)) + + +def test_gen_rejects_an_impure_caller_of_a_class_guarded_callee(): + setter = f_setl() + caller = fn("caller", [Param("x", OBJ)], NONE, {}, + [ir.ExprStmt(Call(NONE, "setl", (L("x", OBJ), L("x", OBJ)))), Return()], + pure=False, may_deopt=False) + from typedpython import cgen + with pytest.raises(cgen.CGenError, match="deopt"): + generate("tp_gen_cls_bad3", funcs=(setter, caller)) + + +def test_gen_rejects_unknown_class_field_or_arity(): + from typedpython import cgen + unknown = fn("u", [Param("x", OBJ)], OBJ, {}, [Return(New(OBJ, "Nope", ()))], + pure=True, may_deopt=False) + badfield = fn("f", [Param("x", OBJ, cls="Node")], OBJ, {}, + [Return(FieldGet(OBJ, L("x", OBJ), "Node", "middle"))], pure=True, may_deopt=True) + arity = fn("a", [], OBJ, {}, [Return(New(OBJ, "Node", (Const(OBJ, None),)))], + pure=True, may_deopt=False) + for i, f in enumerate((unknown, badfield, arity)): + with pytest.raises(cgen.CGenError): + generate(f"tp_gen_cls_bad4_{i}", funcs=(f,)) + + +def test_gen_uses_only_the_runtime_helpers_for_slots(): + text = generate("tp_gen_cls_only") + for h in ("tp_field_get", "tp_field_set", "tp_new_fixed", "tp_is_exact", "tp_class_capture"): + assert h in text, h + assert "PyMember_" not in text and "tp_alloc" not in text and "->tp_dictoffset" not in text + + +# --- refcounts --------------------------------------------------------------------------------------------- + +def test_reference_counts_are_unchanged_over_many_calls(m): + shared = object() + node = m.Node(shared, shared) + bare = m.Node.__new__(m.Node) + + def churn(rounds): + for _ in range(rounds): + m.make(4) + m.left_checked(node) + m.setl(node, shared) + m.is_node(node) + m.same(node, node) + m.check(m.Node(None, None)) + m.check(m.mk_sub(None, None)) # deopt path + try: + m.check(bare) + except AttributeError: + pass + try: + m.setl(5, 5) + except AttributeError: + pass + + churn(20) # warm up caches, then measure + def counts(): + # Free-threaded CPython (#159): a heap type is deferred-refcounted, and sys.getrefcount of it + # returns a flagged value (bit 60) whose count moves as thread-local counts are merged, with + # no change in what holds the type. Only a value without the flag is a real count. + deferred = 1 << 60 + types = tuple(None if (n := sys.getrefcount(t)) >= deferred else n for t in (m.Node, m.Sub)) + return (sys.getrefcount(shared), sys.getrefcount(node), sys.getrefcount(bare)) + types + + base = counts() + churn(3000) + assert counts() == base, (base, counts()) + + +# --- the mutation: without the exact-type guard the subclass test must fail --------------------------- + +MUTANT_SCRIPT = textwrap.dedent('''\ + import sys + sys.path.insert(0, {pkg!r}) + from typedpython import cbuild + m = cbuild.load({name!r}, {so!r}) + sub = m.mk_sub(m.Node(None, None), None) + want = m.__typedpython_interpreted__["check"](sub) + got = m.check(sub) + print("RESULT", want, got, m.__typedpython_deopts__) + bad = 0 if (want == got and m.__typedpython_deopts__ == 1) else 3 + # the DIRECT native call path: a subclass / a non-Node reached as a child of a Node + def outcome(f, x): + try: + return ("ok", f(x)) + except Exception as e: + return (type(e).__name__, str(e)) + for child in (sub, 5, m.Fat(None, None)): + tree = m.Node(child, None) + w = outcome(m.__typedpython_interpreted__["check"], tree) + before = m.__typedpython_deopts__ + g = outcome(m.check, tree) + print("NESTED", type(child).__name__, w, g, m.__typedpython_deopts__ - before) + if w != g or m.__typedpython_deopts__ <= before: + bad = 3 + sys.exit(bad) + ''') + + +def run_child(name, so): + script = MUTANT_SCRIPT.format(pkg=str(PACKAGE.parent), name=name, so=str(so)) + return subprocess.run([sys.executable, "-c", script], capture_output=True, text=True) + + +def test_mutation_unmutated_build_passes_the_subclass_script(): + need_runtime() + _, so = build("tp_cgen_cls_ok") + run = run_child("tp_cgen_cls_ok", so) + assert run.returncode == 0, run.stdout + run.stderr + + +def test_mutation_without_the_param_guard_the_subclass_test_fails(monkeypatch): + need_runtime() + from typedpython import cgen, cbuild + monkeypatch.setattr(cgen, "_param_cls_fail", lambda *a, **k: "0") # the guard never fires + # Since the per-access class check (test_cgen_class_changes.py) every FieldGet also tests + # `type(obj) is C` itself, so the entry guard alone is no longer the only defence: the mutant + # removes both (with the per-access check kept, this mutant computes the right results). + monkeypatch.setattr(cgen, "_class_current_expr", lambda *a, **k: "1") + src = generate("tp_cgen_cls_mut") + monkeypatch.undo() + so = cbuild.build(src, "tp_cgen_cls_mut", OUT_ROOT / "tp_cgen_cls_mut", runtime_dir=runtime_dir()) + run = run_child("tp_cgen_cls_mut", so) + assert run.returncode != 0, ("the mutant (no exact-type guard on Param.cls) passed the subclass " + "test:\n" + run.stdout + run.stderr) + + +# --- sanitizers --------------------------------------------------------------------------------------------- + +ASAN_SCRIPT = textwrap.dedent('''\ + import sys + sys.path.insert(0, {pkg!r}) + from typedpython import cbuild + m = cbuild.load({name!r}, {so!r}) + I = m.__typedpython_interpreted__ + for d in (0, 1, 6): + t = m.make(d) + assert m.check(t) == I["check"](t) == 2 ** d - 1 + for _ in range(300): + t = m.make(5) + n = m.Node(t, t) + assert m.check(n) == 2 * 31 + 1 + m.setl(n, None) + m.left_checked(n) + sub = m.mk_sub(m.Node(None, None), None) + assert m.check(sub) == 1 and m.check(m.Node(sub, None)) == I["check"](m.Node(sub, None)) + for bad in (5, "x", object(), m.Fat(None, None)): + try: + m.check(bad) + except AttributeError: + pass + try: + m.setl(bad, 1) + except AttributeError: + pass + bare = m.Node.__new__(m.Node) + try: + m.check(bare) + except AttributeError: + pass + assert m.check_fat(m.mkfat(3)) == 7 + assert m.left_of_prop(m.Prop()) == "shadowed" + print("ASAN-RUN-OK", m.__typedpython_deopts__) + ''') + + +def test_class_functions_under_address_and_undefined_sanitizers(): + need_runtime() + cc = sysconfig.get_config_var("CC") or "cc" + if sys.platform != "darwin": + pytest.skip("sanitizer preload recipe written for macOS only") + probe = subprocess.run(cc.split() + ["-print-file-name=libclang_rt.asan_osx_dynamic.dylib"], + capture_output=True, text=True) + if probe.returncode != 0 or not Path(probe.stdout.strip()).is_file(): + pytest.skip(f"host compiler {cc!r} has no ASan runtime ({probe.stdout.strip()!r})") + from typedpython import cbuild + name = "tp_cgen_cls_asan" + flags = list(cbuild.DEFAULT_FLAGS) + ["-O1", "-g", "-fno-omit-frame-pointer", + "-fsanitize=address,undefined", + "-fno-sanitize-recover=undefined"] + so = cbuild.build(generate(name), name, OUT_ROOT / name, runtime_dir=runtime_dir(), flags=flags) + d = so.parent + launcher_c, launcher = d / "asan_python.c", d / "asan_python" + launcher_c.write_text("#include \nint main(int argc, char **argv) " + "{ return Py_BytesMain(argc, argv); }\n") + link = subprocess.run(cc.split() + ["-fsanitize=address,undefined", "-g", + f"-I{sysconfig.get_paths()['include']}", str(launcher_c), + f"-L{sysconfig.get_config_var('LIBDIR')}", + f"-lpython{sysconfig.get_config_var('LDVERSION')}", + "-o", str(launcher)], capture_output=True, text=True) + if link.returncode != 0: + pytest.skip(f"cannot link a sanitized launcher: {link.stderr[-2000:]}") + env = dict(os.environ, PYTHONHOME=sys.base_prefix, + ASAN_OPTIONS="detect_leaks=0:abort_on_error=0:halt_on_error=1", + UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1") + env.pop("DYLD_INSERT_LIBRARIES", None) + script = ASAN_SCRIPT.format(pkg=str(PACKAGE.parent), name=name, so=str(so)) + run = subprocess.run([str(launcher), "-c", script], capture_output=True, text=True, env=env) + out = run.stdout + run.stderr + if "violates platform policy" in out or "Interceptors are not working" in out: + pytest.skip(f"ASan runtime could not be loaded: {out[-2000:]}") + assert run.returncode == 0 and "ASAN-RUN-OK" in run.stdout, out[-6000:] + linked = subprocess.run(["otool", "-L", str(so)], capture_output=True, text=True).stdout + assert "libclang_rt.asan" in linked, linked + assert "runtime error" not in out and "AddressSanitizer" not in out, out[-6000:] diff --git a/python-multiplatform-ksp/src/test/python/test_cost_rule.py b/python-multiplatform-ksp/src/test/python/test_cost_rule.py new file mode 100644 index 000000000..f74112452 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_cost_rule.py @@ -0,0 +1,135 @@ +"""The cost policy (cost.py, #150): a function is compiled only if it does native work. + +A verified function that only does object operations in its loops (in an impure function an int is a +CPython object, so `+`, `//`, `%` and `>` are generic PyNumber / RichCompare calls) is slower compiled +than interpreted, so it stays interpreted with a diagnostic. Pure functions are never touched. +`pipeline.compile_module(..., cost_rule=False)` is the test-only opt-out. +""" +from __future__ import annotations + +import textwrap + +import pytest + +from typedpython import cost, pipeline + +HEADER = "# typedpython: compiled\n" + +OBJECT_ONLY = HEADER + textwrap.dedent(''' + def word_of(word_id: int) -> str: + x: int = word_id + 17576 + w: str = "" + while x > 0: + w = chr(97 + x % 26) + w + x //= 26 + return w +''') + + +def build(tmp_path, source, **kw): + path = tmp_path / "m.py" + path.write_text(source) + return pipeline.compile_module(path, tmp_path / "out", **kw) + + +def names(result): + return {f.name for f in result.module.functions} + + +def test_an_object_only_loop_stays_interpreted_with_the_reason(tmp_path): + r = build(tmp_path, OBJECT_ONLY) + assert names(r) == set() and r.extension is None + assert "slower than the interpreter" in r.skipped["word_of"] + assert r.skipped["word_of"].startswith("policy/cost: only object operations in its loops") + assert [d.rule for d in r.diagnostics] == ["policy/cost"] + + +def test_the_opt_out_compiles_the_same_function(tmp_path): + r = build(tmp_path, OBJECT_ONLY, cost_rule=False) + assert names(r) == {"word_of"} and r.extension is not None + + +MIXED = HEADER + textwrap.dedent(''' + def mixed(n: int, xs: list[str]) -> float: + total: float = 0.0 + for i in range(n): + total += 1.5 * 2.0 + xs.append("a") + return total + + + def sq(k: int) -> int: + return k * k + + + def pure_no_loop(a: float, b: float) -> float: + return a * b + 1.0 + + + def object_loop_with_native_op_outside(n: int, xs: list[str]) -> float: + total: float = 1.0 + 2.0 + i: int = 0 + while i < n: + xs.append("a") + i += 1 + return total + + + def native_loop_inside_object_loop(rows: list[list[str]]) -> float: + total: float = 0.0 + for row in rows: + for k in range(3): + total += 0.5 * 2.0 + return total +''') + + +def test_one_native_loop_with_object_operations_compiles(tmp_path): + r = build(tmp_path, MIXED) + got = names(r) + assert {"mixed", "pure_no_loop", "native_loop_inside_object_loop"} <= got, r.skipped + assert "object_loop_with_native_op_outside" not in got # the native op is outside the loop + assert "object_loop_with_native_op_outside" in r.skipped + + +def test_a_pure_function_is_never_rejected_even_without_a_loop(tmp_path): + r = build(tmp_path, MIXED) + assert "pure_no_loop" in names(r) and "sq" in names(r) + + +def test_a_caller_of_a_rejected_function_is_rejected_too(tmp_path): + # unit level: the propagation over direct Calls + from typedpython import frontend, ir, verify + src = tmp_path / "p.py" + src.write_text(OBJECT_ONLY) + proved, diags = verify.verify(frontend.lower(src)) + assert [f.name for f in proved.functions] == ["word_of"] + word = proved.functions[0] + caller = ir.Function(name="c", params=(), returns=ir.Type.NONE, locals={}, pure=False, + may_deopt=False, source_line=1, + body=(ir.ForRange("i", ir.Const(ir.Type.I64, 0), ir.Const(ir.Type.I64, 3), + ir.Const(ir.Type.I64, 1), + (ir.ExprStmt(ir.Call(ir.Type.NONE, "word_of", ())),)),)) + module = ir.Module("p", (word, caller)) + out, diags = cost.apply(module, []) + assert out.functions == () # `c` has no native op (an empty-arg Call is not one) + assert "word_of" in out.skipped and "c" in out.skipped + + +def test_the_rule_is_what_keeps_it_interpreted(tmp_path, monkeypatch): + """Mutation: with the rule replaced by 'always native' the first test above must fail.""" + monkeypatch.setattr(cost, "does_native_work", lambda fn: True) + with pytest.raises(AssertionError): + test_an_object_only_loop_stays_interpreted_with_the_reason(tmp_path) + + +STRAIGHT_LINE = HEADER + textwrap.dedent(''' + def label(n: int, name: str) -> str: + return f"{name}:{n + 1}" +''') + + +def test_a_straight_line_impure_function_still_compiles(tmp_path): + # no loop to lose to the interpreter: what compiled before #150 keeps compiling + r = build(tmp_path, STRAIGHT_LINE) + assert names(r) == {"label"}, r.skipped diff --git a/python-multiplatform-ksp/src/test/python/test_eval_breaker.py b/python-multiplatform-ksp/src/test/python/test_eval_breaker.py new file mode 100644 index 000000000..45f40bb67 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_eval_breaker.py @@ -0,0 +1,159 @@ +"""Compiled code checks the eval breaker (#141, SPEC N-8): during a long compiled loop or recursion, +signals reach Python (KeyboardInterrupt), and other threads get the GIL. + +A Python signal handler, a pending call and another thread are user code that can change lists and +globals. A function that holds an array copy-in (ArrayParam) or an entry-globals snapshot runs no +user code by contract, so while such a function is active the poll is deferred: the result is then +the CPython execution in which the signal arrived after the call. + +Each scenario runs in a child interpreter, so a broken build cannot take pytest down and signal +handlers stay out of the test process. Timing thresholds are generous; they separate "polled" +(tens of ms) from "never polled" (the whole loop, seconds). +""" +import json +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + +from typedpython import pipeline + +SOURCE = """\ +# typedpython: compiled + + +def spin(n: int) -> float: + acc: float = 0.0 + for k in range(n): + acc += k * 0.5 + return acc + + +def fib(n: int) -> int: + if n < 2: + return n + return fib(n - 1) + fib(n - 2) + + +def total(xs: list[float], rounds: int) -> float: + s: float = 0.0 + for r in range(rounds): + for i in range(len(xs)): + s += xs[i] + return s +""" + +LOADER = """\ +import importlib.machinery, importlib.util, json, signal, sys, threading, time +path = sys.argv[1] +loader = importlib.machinery.ExtensionFileLoader("loops", path) +spec = importlib.util.spec_from_file_location("loops", path, loader=loader) +m = importlib.util.module_from_spec(spec) +loader.exec_module(m) +""" + + +@pytest.fixture(scope="module") +def extension(tmp_path_factory): + d = tmp_path_factory.mktemp("eb") + path = d / "loops.py" + path.write_text(SOURCE) + result = pipeline.compile_module(path, d / "out") + assert result.extension is not None, result.skipped + names = {f.name for f in result.module.functions} + assert names >= {"spin", "fib", "total"}, result.skipped + return result.extension + + +def run_child(extension: Path, body: str) -> dict: + script = LOADER + textwrap.dedent(body) + proc = subprocess.run([sys.executable, "-c", script, str(extension)], + capture_output=True, text=True, timeout=120) + assert proc.returncode == 0, f"child failed ({proc.returncode}):\n{proc.stdout}\n{proc.stderr}" + return json.loads(proc.stdout.strip().splitlines()[-1]) + + +def test_keyboard_interrupt_reaches_a_compiled_loop(extension): + r = run_child(extension, """ + threading.Timer(0.05, lambda: __import__("_thread").interrupt_main()).start() + t0 = time.perf_counter() + try: + m.spin(4_000_000_000) # seconds of work if never interrupted + out = "completed" + except KeyboardInterrupt: + out = "interrupted" + print(json.dumps({"out": out, "elapsed": time.perf_counter() - t0})) + """) + assert r["out"] == "interrupted" + assert r["elapsed"] < 1.0, r + + +def test_keyboard_interrupt_reaches_compiled_recursion(extension): + r = run_child(extension, """ + threading.Timer(0.05, lambda: __import__("_thread").interrupt_main()).start() + t0 = time.perf_counter() + try: + m.fib(45) # many seconds compiled; no loop, only calls + out = "completed" + except KeyboardInterrupt: + out = "interrupted" + print(json.dumps({"out": out, "elapsed": time.perf_counter() - t0})) + """) + assert r["out"] == "interrupted" + assert r["elapsed"] < 1.0, r + + +def test_another_thread_runs_while_a_compiled_loop_runs(extension): + r = run_child(extension, """ + stamps = [] + stop = False + def worker(): + n = 0 + while not stop: + n += 1 + if n % 1000 == 0: + stamps.append(time.perf_counter()) + t = threading.Thread(target=worker) + t.start() + time.sleep(0.05) + t0 = time.perf_counter() + m.spin(1_500_000_000) + t1 = time.perf_counter() + stop = True + t.join() + inside = [s for s in stamps if t0 + 0.1 < s < t1 - 0.1] + print(json.dumps({"inside": len(inside), "call_s": t1 - t0})) + """) + assert r["call_s"] > 0.3, f"loop too short to judge: {r}" + assert r["inside"] > 0, f"the worker thread never ran during the compiled call: {r}" + + +def test_a_handler_is_deferred_while_an_array_copy_is_live(extension): + # total() reads its list through a copy-in. A handler that writes the list mid-call would be + # invisible to the copy, a result no CPython execution can give. So the handler must run after + # the call (the execution where the signal arrived late), or the result must include its write. + r = run_child(extension, """ + xs = [1.0] * 100_000 + seen = {} + def handler(signum, frame): + seen["t"] = time.perf_counter() + xs[0] = 1.0e6 + signal.signal(signal.SIGUSR1, handler) + import os + threading.Timer(0.05, lambda: os.kill(os.getpid(), signal.SIGUSR1)).start() + t0 = time.perf_counter() + s = m.total(xs, 20_000) + t1 = time.perf_counter() + time.sleep(0.01) # let a deferred handler run + print(json.dumps({"s": s, "plain": 100_000 * 20_000 * 1.0, "t0": t0, "t1": t1, + "handler": seen.get("t")})) + """) + assert r["handler"] is not None, "the handler never ran" + assert r["t1"] - r["t0"] > 0.3, f"call too short to judge: {r}" + if r["s"] == r["plain"]: + # The deferred handler runs at the first eval-breaker check after the call returns, which can + # come a few microseconds BEFORE the child reads t1 (3.15t checks right after a call): the + # tolerance only has to separate that from "ran inside a call of 0.3 s or more". + assert r["handler"] >= r["t1"] - 0.005, f"handler ran inside the call but its write is not in the result: {r}" diff --git a/python-multiplatform-ksp/src/test/python/test_fast_entry.py b/python-multiplatform-ksp/src/test/python/test_fast_entry.py new file mode 100644 index 000000000..7adc7d2c9 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_fast_entry.py @@ -0,0 +1,395 @@ +"""TypedPython fast entry (issue #147): a depth precheck plus an uncounted entry for *bounded* +compiled functions (no call cycle, bounded callees, no node that can run Python code). + + * the precheck `tp_depth_room(k)` is the exact condition under which the per-call counting of + `tp_enter_call` could not refuse anything, so the point where RecursionError is raised is the + interpreter's, at every depth (differential test, in a child interpreter with a small limit); + * the fast path is really taken when there is room (a hit counter compiled in only with + `-DTP_TRACE_FAST`) and is not taken when there is none; + * functions that are not bounded get no fast impl; + * the generated C shape (the fast impl is `static inline`, calls only fast impls, the normal impl + keeps counting). + +Builds go through the front end, verifier, cgen and cbuild in `.tmp/` of the worktree via tmp_path. +""" +import json +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + +from typedpython import cbuild, cgen, frontend, verify + +SOURCE = """\ +# typedpython: compiled + + +def helper(x: int) -> float: + d: dict[str, int] = {} + d['a'] = x + return d['a'] * 2.5 + + +def leaf(i: int) -> float: + return 1.0 / (i + 1) + + +def mid(i: int) -> float: + return leaf(i) + leaf(i + 1) + + +def top(i: int) -> float: + return mid(i) * 2.0 + + +def objy(i: int) -> float: + return float(helper(i)) + leaf(i) + + +def viaobj(i: int) -> float: + return objy(i) + 1.0 + + +def rec(n: int) -> int: + if n == 0: + return 0 + return 1 + rec(n - 1) + + +def callrec(n: int) -> int: + return rec(n) + 1 + + +def loopy(n: int) -> float: + s: float = 0.0 + for i in range(n): + s += leaf(i) + return s + + +def outer(n: int) -> float: + return loopy(n) + 1.0 + + +def cube(x: int) -> float: + return float(x * x * x) + leaf(x) + + +def fill(x: int, out: list[float]) -> None: + out[0] = cube(x) +""" + +BOUNDED = {"leaf": 1, "mid": 2, "top": 3, "loopy": 2, "outer": 3, "cube": 2, "fill": 3} +NOT_BOUNDED = ("objy", "viaobj", "rec", "callrec") + + +def build(d: Path, extra: tuple[str, ...] = ()): + path = d / "fast_mod.py" + path.write_text(SOURCE) + proved, _ = verify.verify(frontend.lower(path)) + c = cgen.generate(proved, path, path.name) + so = cbuild.build(c, path.stem, d / "out", flags=cbuild.DEFAULT_FLAGS + extra) + return proved, c, so + + +@pytest.fixture(scope="module") +def traced(tmp_path_factory): + return build(tmp_path_factory.mktemp("fast_traced"), ("-DTP_TRACE_FAST",)) + + +@pytest.fixture(scope="module") +def plain_build(tmp_path_factory): + return build(tmp_path_factory.mktemp("fast_plain")) + + +def fn_text(c: str, signature_start: str) -> str: + """The text of the C function whose definition starts with `signature_start`.""" + i = c.index(signature_start + "(", c.index("static const char tp_source")) + # skip forward declarations: find the definition (the one followed by a newline and `{`) + while not c[c.index(")", i) + 1:].startswith("\n{"): + i = c.index(signature_start + "(", i + 1) + return c[i:c.index("\n}\n", i)] + + +def test_every_function_compiles(traced): + proved = traced[0] + names = {f.name for f in proved.functions} + assert names == set(BOUNDED) | set(NOT_BOUNDED), proved.skipped + + +def test_bounded_functions_get_a_static_inline_fast_impl(traced): + c = traced[1] + for name in BOUNDED: + assert f"static inline int tp_fimpl_{name}(" in c, name + + +def test_unbounded_functions_get_no_fast_impl(traced): + c = traced[1] + for name in NOT_BOUNDED: + assert f"tp_fimpl_{name}" not in c, name + + +def test_the_fast_impl_calls_only_fast_impls_and_never_counts(traced): + c = traced[1] + fast = fn_text(c, "static inline int tp_fimpl_mid") + assert "tp_fimpl_leaf(" in fast and "tp_impl_leaf(" not in fast + assert "tp_enter_call" not in fast and "tp_leave_call" not in fast + assert "tp_poll(tp_st->breaker)" not in fast # no entry poll + loop = fn_text(c, "static inline int tp_fimpl_loopy") + assert "tp_poll(" in loop or "tp_pc" in loop # loop polls stay + + +def test_the_normal_impl_prechecks_and_still_counts(traced): + c = traced[1] + normal = fn_text(c, "static int tp_impl_mid") + assert "tp_depth_room(2)" in normal + assert normal.index("tp_depth_room(2)") < normal.index("tp_enter_call()") + assert "tp_impl_leaf(" in normal and "tp_fimpl_leaf(" not in normal + top = fn_text(c, "static int tp_impl_top") + assert "tp_depth_room(3)" in top + assert "tp_depth_room" not in fn_text(c, "static int tp_impl_rec") + + +def test_the_fast_impl_is_never_addressed(traced): + c = traced[1] + assert "&tp_fimpl_" not in c and "(tp_fimpl_" not in c + + +LOAD = """\ +import importlib.machinery, importlib.util, json, sys +def load(name, path, ext): + if ext: + loader = importlib.machinery.ExtensionFileLoader(name, path) + spec = importlib.util.spec_from_file_location(name, path, loader=loader) + else: + spec = importlib.util.spec_from_file_location(name, path) + m = importlib.util.module_from_spec(spec) + (loader if ext else spec.loader).exec_module(m) + return m +comp = load("fast_mod", sys.argv[1], True) +plain = load("fast_mod_plain", sys.argv[2], False) +LIMIT = int(sys.argv[3]) +sys.setrecursionlimit(LIMIT) +""" + +DESCEND = """\ +def descend(d, fn, a): + if d > 0: + return descend(d - 1, fn, a) + try: + return ("ok", fn(a)) + except RecursionError: + return ("rec",) + +def run(fn, a, d): + try: + return descend(d, fn, a) + except RecursionError: + return ("deep",) +""" + +CHILD = LOAD + DESCEND + """\ +report = {"mismatch": [], "ok": {}, "rec": {}, "hits": []} +for name in ("leaf", "mid", "top", "loopy"): + arg = 3 + for d in range(1, LIMIT + 6): + before = comp.__tp_trace_fast_hits__() + c = run(getattr(comp, name), arg, d) + hit = comp.__tp_trace_fast_hits__() - before + p = run(getattr(plain, name), arg, d) + if c != p: + report["mismatch"].append([name, d, c, p]) + kind = c[0] + report.setdefault(kind, {}) + report[kind][name] = report[kind].get(name, 0) + 1 + # one hit per entry from Python that found room, none when it ran counted (or never ran) + if hit != (1 if kind == "ok" else 0): + report["hits"].append([name, d, kind, hit]) +print(json.dumps(report)) +""" + + +def test_differential_depth_at_every_depth_around_the_limit(traced, tmp_path): + proved, c, so = traced + plain_src = tmp_path / "fast_mod_plain.py" + plain_src.write_text(SOURCE) + proc = subprocess.run([sys.executable, "-c", CHILD, str(so), str(plain_src), "200"], + capture_output=True, text=True, timeout=300) + assert proc.returncode == 0, proc.stdout + proc.stderr + r = json.loads(proc.stdout.strip().splitlines()[-1]) + assert r["mismatch"] == [], r["mismatch"][:5] + assert r["hits"] == [], r["hits"][:5] + for name in ("leaf", "mid", "top", "loopy"): # the sweep crossed the boundary + assert r["ok"][name] > 100 and r["rec"][name] >= 1, (name, r) + # top needs more room than leaf: it starts raising at a smaller depth + assert r["rec"]["top"] > r["rec"]["leaf"] + + +CHILD_REDO = LOAD + DESCEND + """\ +def mk(mod): + def call(x): + out = [0.0] + mod.fill(x, out) + return out[0] + return call + +def deep_comp(i): + return float(comp.rec(50)) + +def deep_plain(i): + return float(plain.rec(50)) + +report = {"mismatch": [], "ok": {}, "rec": {}, "deopts": 0} +for x in (3, 3_000_000): # 3: no overflow; 3_000_000: x * x * x overflows i64, the redo runs + if x != 3: + # the interpreted cube of the redo looks `leaf` up in its module globals: make that a deep + # call, so that what the redo runs depends on the exact depth it starts from + comp.leaf = deep_comp + plain.leaf = deep_plain + ok = 0 + before = comp.__typedpython_deopts__ + for d in range(1, LIMIT + 6): + c = run(mk(comp), x, d) + p = run(mk(plain), x, d) + if c != p: + report["mismatch"].append([x, d, c, p]) + kind = c[0] + report.setdefault(kind, {}) + report[kind][str(x)] = report[kind].get(str(x), 0) + 1 + ok += kind == "ok" + if x != 3: + report["deopts"] = comp.__typedpython_deopts__ - before + report["deopt_ok"] = ok +print(json.dumps(report)) +""" + +CHILD_SIGNAL = LOAD + DESCEND + """\ +import signal + +class Stop(Exception): + pass + +M = None +res = [] + +def measure(): + lo, hi = 0, 400 + while lo < hi: + mid = (lo + hi + 1) // 2 + try: + M.rec(mid) + lo = mid + except RecursionError: + hi = mid - 1 + return lo + +def handler(sig, frm): + res.append(measure()) + raise Stop + +signal.signal(signal.SIGALRM, handler) + +# interpreted baseline: outer_py -> spin_py -> trip, the handler runs in trip's frame, where the +# compiled run has the breaker lambda (outer and spin are compiled frames with no Python frame) +def trip(): + signal.raise_signal(signal.SIGALRM) + +def spin_py(): + trip() + +def outer_py(a): + spin_py() + +def base(d): + global M + M = plain + res.clear() + try: + descend(d, outer_py, 0) + except Stop: + pass + return list(res) + +def compiled(d): + global M + M = comp + res.clear() + signal.setitimer(signal.ITIMER_REAL, 0.05) + try: + descend(d, comp.outer, 10 ** 9) # outer -> loopy: the poll runs in a fast tree at fd 2 + except Stop: + pass + signal.setitimer(signal.ITIMER_REAL, 0) + return list(res) + +report = {"mismatch": [], "values": []} +for d in range(0, 150, 7): + b, c = base(d), compiled(d) + report["values"].append(b) + if b != c or len(b) != 1: + report["mismatch"].append([d, b, c]) +print(json.dumps(report)) +""" + + +def run_child(script, so, plain_src, limit="200"): + proc = subprocess.run([sys.executable, "-c", script, str(so), str(plain_src), limit], + capture_output=True, text=True, timeout=300) + assert proc.returncode == 0, proc.stdout + proc.stderr + return json.loads(proc.stdout.strip().splitlines()[-1]) + + +def test_a_redo_inside_a_fast_tree_sees_the_exact_depth(traced, tmp_path): + # fill (impure, bounded) -> cube (bounded, deopts on i64 overflow): the redo runs the interpreted + # cube, whose `leaf` (rebound by the child to a call of the compiled recursive rec(50)) goes + # through the counted path; the depth at which that raises RecursionError must be the + # interpreter's, so the redo must be counted with the frames of the fast tree around it + proved, c, so = traced + plain_src = tmp_path / "fast_mod_plain.py" + plain_src.write_text(SOURCE) + r = run_child(CHILD_REDO, so, plain_src) + assert r["mismatch"] == [], r["mismatch"][:5] + for x in ("3", "3000000"): + assert r["ok"][x] > 100 and r["rec"][x] >= 1, (x, r) + assert r["deopts"] >= r["deopt_ok"] > 100 # the redo really ran at every ok depth + assert "tp_depth_py_begin(tp_fd);" in fn_text(c, "static inline int tp_fimpl_fill") + + +def test_a_signal_handler_in_a_fast_tree_poll_sees_the_exact_depth(traced, tmp_path): + # a compiled recursive call made by a signal handler that the poll of a loop two frames deep + # in a fast tree runs reaches RecursionError at the interpreter's depth + proved, c, so = traced + plain_src = tmp_path / "fast_mod_plain.py" + plain_src.write_text(SOURCE) + r = run_child(CHILD_SIGNAL, so, plain_src) + assert r["mismatch"] == [], r["mismatch"][:5] + assert len({v[0] for v in r["values"]}) > 5 # the measured depth tracks the stack depth + loop = fn_text(c, "static inline int tp_fimpl_loopy") + assert "tp_depth_py_begin(tp_fd);" in loop and "tp_depth_py_end(tp_fd);" in loop + + +def test_the_fast_path_is_taken_when_there_is_room(tmp_path): + proved, c, so = build(tmp_path, ("-DTP_TRACE_FAST",)) + m = cbuild.load("fast_mod", so) + assert m.__tp_trace_fast_hits__() == 0 + assert m.top(4) == 2.0 * (1 / 5 + 1 / 6) + assert m.__tp_trace_fast_hits__() == 1 # one precheck, no per-callee counting + assert m.loopy(10) == sum(1.0 / (i + 1) for i in range(10)) + assert m.__tp_trace_fast_hits__() == 2 + assert m.rec(10) == 10 and m.callrec(10) == 11 # not bounded: no precheck + assert m.__tp_trace_fast_hits__() == 2 + assert m.viaobj(3) == 3 * 2.5 + 1 / 4 + 1.0 + assert m.__tp_trace_fast_hits__() == 3 # objy is not bounded; its callee leaf is + assert m.mid(2) == 1 / 3 + 1 / 4 # a bounded callee entered from Python + assert m.__tp_trace_fast_hits__() == 4 + + +def test_the_trace_counter_is_absent_from_a_normal_build(plain_build): + c = plain_build[1] + assert "TP_TRACE_FAST" in c # guarded, so compiled out + so = plain_build[2] + m = cbuild.load("fast_mod", so) + assert not hasattr(m, "__tp_trace_fast_hits__") + assert m.top(1) == 2.0 * (1 / 2 + 1 / 3) diff --git a/python-multiplatform-ksp/src/test/python/test_free_threaded.py b/python-multiplatform-ksp/src/test/python/test_free_threaded.py new file mode 100644 index 000000000..6acf69f63 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_free_threaded.py @@ -0,0 +1,727 @@ +"""Compiled modules are safe on free-threaded CPython 3.15t (python-multiplatform #159, #141 phase 2). + +Free-threaded 3.15t is the only build (maintainer decision 2026-10-04). A generated extension must + * declare `Py_mod_gil = Py_MOD_GIL_NOT_USED`, so importing it does not switch the GIL back on; + * keep its shared mutable state thread-safe (runtime/API.md "Free-threaded CPython"); + * read nothing through a borrowed reference that another thread can free; + * hold the list's critical section from before the copy-in to after the write-back (array params). + +Every concurrency scenario runs in a CHILD interpreter with a timeout (a crash or a deadlock fails +one test, not the run). Iteration counts scale with TP_FT_STRESS (default 1). + +Red record (before the change, on the 3.15t venv): the GIL test and the three module-level checks +failed (`Py_mod_gil` absent, the GIL came back at import), the concurrent counters lost updates, and +a three-list function was compiled. +""" +from __future__ import annotations + +import dataclasses +import json +import os +import re +import subprocess +import sys +import sysconfig +import textwrap +from pathlib import Path + +import pytest + +from typedpython import cbuild, cgen, frontend, pipeline, verify + +STRESS = max(1, int(os.environ.get("TP_FT_STRESS", "1"))) +FREE_THREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + +SOURCE = '''\ +# typedpython: compiled + + +class Cell: + __slots__ = ("a", "b") + a: object + b: object + + def __init__(self, a: object, b: object) -> None: + self.a = a + self.b = b + + +class Twin: + __slots__ = ("a", "b") + a: object + b: object + + def __init__(self, a: object, b: object) -> None: + self.a = a + self.b = b + + +def geta(c: Cell) -> object: + return c.a + + +def seta(c: Cell, v: object) -> None: + c.a = v + + +def mk(a: object, b: object) -> Cell: + return Cell(a, b) + + +def total(xs: list[float], rounds: int) -> float: + s: float = 0.0 + for r in range(rounds): + for i in range(len(xs)): + s += xs[i] + return s + + +def fill(xs: list[float], v: float) -> None: + for i in range(len(xs)): + xs[i] = v + + +def edge(xs: list[float]) -> float: + return xs[0] - xs[len(xs) - 1] + + +def fill_int(xs: list[int], v: int) -> None: + for i in range(len(xs)): + xs[i] = v + + +def edge_int(xs: list[int]) -> int: + return xs[0] - xs[len(xs) - 1] + + +def rotate(xs: list[float]) -> None: + if len(xs) > 1: + first: float = xs[0] + for i in range(1, len(xs)): + xs[i - 1] = xs[i] + xs[len(xs) - 1] = first + + +def zero2(xs: list[int], ys: list[int]) -> int: + for i in range(len(xs)): + xs[i] = 0 + for i in range(len(ys)): + ys[i] = 0 + return 0 + + +def pair_edge(xs: list[int], ys: list[int]) -> int: + return xs[0] - ys[len(ys) - 1] + + +def three(a: list[int], b: list[int], c: list[int]) -> int: + return len(a) + len(b) + len(c) + + +K: int = 3 + + +def usek(x: int) -> int: + return x + K +''' + +LOADER = """\ +import faulthandler, importlib.machinery, importlib.util, json, sys, threading, time +faulthandler.enable() +path = sys.argv[1] +loader = importlib.machinery.ExtensionFileLoader("ftmod", path) +spec = importlib.util.spec_from_file_location("ftmod", path, loader=loader) +m = importlib.util.module_from_spec(spec) +loader.exec_module(m) +STRESS = int(sys.argv[2]) + + +def run_threads(targets): + errors = [] + + def wrap(fn): + def go(): + try: + fn() + except BaseException as e: # a failed assertion in a worker must reach the report + errors.append(repr(e)) + return go + + ts = [threading.Thread(target=wrap(t)) for t in targets] + for t in ts: + t.start() + for t in ts: + t.join() + return errors +""" + + +@pytest.fixture(scope="module") +def built(tmp_path_factory): + d = tmp_path_factory.mktemp("ft") + path = d / "ftmod.py" + path.write_text(SOURCE) + result = pipeline.compile_module(path, d / "out", cost_rule=False) # object-only setters: the cost policy would skip them (#150) + assert result.extension is not None, result.skipped + compiled = {f.name for f in result.module.functions} + wanted = {"geta", "seta", "mk", "total", "fill", "edge", "fill_int", "edge_int", "rotate", "zero2", + "pair_edge", "usek"} + assert wanted <= compiled, {n: result.skipped.get(n) for n in wanted - compiled} + return result, path, d + + +@pytest.fixture(scope="module") +def extension(built): + return built[0].extension + + +def run_child(extension: Path, body: str, timeout: int = 180, env: dict | None = None) -> dict: + script = LOADER + textwrap.dedent(body) + e = dict(os.environ) + e.pop("PYTHON_GIL", None) + if os.environ.get("TP_FT_FORCE_GIL_OFF") == "1": # red record: what the code does with no GIL + e["PYTHON_GIL"] = "0" + e.update(env or {}) + proc = subprocess.run([sys.executable, "-c", script, str(extension), str(STRESS)], + capture_output=True, text=True, timeout=timeout, env=e) + assert proc.returncode == 0, f"child failed ({proc.returncode}):\n{proc.stdout}\n{proc.stderr}" + return json.loads(proc.stdout.strip().splitlines()[-1]) + + +pytestmark = pytest.mark.skipif(not FREE_THREADED, reason="needs a free-threaded CPython (3.15t)") + + +# --- the module declares it does not need the GIL ---------------------------------------------------- + +def test_generated_module_declares_gil_not_used(built): + result, path, _ = built + c_text = cgen.generate(result.module, path) + assert re.search(r"\{\s*Py_mod_gil\s*,\s*Py_MOD_GIL_NOT_USED\s*\}", c_text) + assert "Py_GIL_DISABLED" in c_text # guarded: a GIL build still compiles + + +def test_importing_a_compiled_module_keeps_the_gil_off(extension): + r = run_child(extension, """ + print(json.dumps({"before": True, "gil": sys._is_gil_enabled(), + "f": m.total([1.0, 2.0], 1)})) + """) + assert r["gil"] is False and r["f"] == 3.0 + + +def test_no_runtime_warning_about_the_gil(extension): + script = LOADER.replace("import faulthandler", "import warnings\nwarnings.simplefilter('error')\n" + "import faulthandler") + proc = subprocess.run([sys.executable, "-c", script + "print('ok')", str(extension), "1"], + capture_output=True, text=True, timeout=120) + assert proc.returncode == 0, proc.stderr + assert "global interpreter lock" not in proc.stderr + + +# --- more than two list parameters stay interpreted -------------------------------------------------- + +def test_three_list_parameters_stay_interpreted(built): + result, _, _ = built + assert "three" not in {f.name for f in result.module.functions} + reason = result.skipped["three"] + assert "list" in reason and ("three" in reason or "more than two" in reason), reason + + +def test_front_end_gives_a_clear_reason(tmp_path): + p = tmp_path / "m.py" + p.write_text("# typedpython: compiled\n\n\ndef f(a: list[int], b: list[float], c: list[int]) -> int:\n" + " return len(a)\n") + m = frontend.lower(p) + assert not m.functions + assert "more than two list parameters" in m.skipped["f"], m.skipped["f"] + + +def test_two_list_parameters_still_compile(built): + result, _, _ = built + assert {"zero2", "pair_edge"} <= {f.name for f in result.module.functions} + + +def test_verifier_rejects_three_array_params(built): + from typedpython import ir + result, _, _ = built + f = next(f for f in result.module.functions if f.name == "zero2") + third = ir.ArrayParam("zs", ir.Type.I64_ARRAY, False) + bad = dataclasses.replace(f, params=f.params + (third,)) + mod = dataclasses.replace(result.module, functions=type(result.module.functions)([bad]), skipped={}) + proved, diags = verify.verify(mod) + assert not proved.functions + assert any("array" in d.message and "two" in d.message for d in diags), diags + + +def test_cgen_refuses_three_array_params(built): + from typedpython import ir + result, path, _ = built + f = next(f for f in result.module.functions if f.name == "zero2") + third = ir.ArrayParam("zs", ir.Type.I64_ARRAY, False) + bad = dataclasses.replace(f, params=f.params + (third,)) + mod = dataclasses.replace(result.module, functions=type(result.module.functions)([bad]), skipped={}) + with pytest.raises(cgen.CGenError, match="two"): + cgen.generate(mod, path) + + +# --- concurrent calls -------------------------------------------------------------------------------- + +def test_private_lists_in_many_threads(extension): + r = run_child(extension, """ + N, ROUNDS = 8, 40 * STRESS + bad = [] + def work(k): + def go(): + xs = [float(k * 1000 + i) for i in range(500)] + want = sum(xs) + for _ in range(ROUNDS): + if m.total(xs, 3) != want * 3: + bad.append(("total", k)) + m.fill(xs, float(k)) + if xs != [float(k)] * 500: + bad.append(("fill", k)) + xs[:] = [float(k * 1000 + i) for i in range(500)] + return go + errs = run_threads([work(k) for k in range(N)]) + print(json.dumps({"bad": bad, "errs": errs})) + """) + assert r == {"bad": [], "errs": []} + + +def test_shared_list_only_read(extension): + r = run_child(extension, """ + shared = [float(i) * 0.5 for i in range(2000)] + want = sum(shared) * 5 + bad = [] + def go(): + for _ in range(60 * STRESS): + v = m.total(shared, 5) + if v != want: + bad.append(v) + errs = run_threads([go for _ in range(8)]) + print(json.dumps({"bad": bad[:3], "errs": errs, "gil": sys._is_gil_enabled()})) + """) + assert r == {"bad": [], "errs": [], "gil": False} + + +def test_shared_list_writers_and_readers_see_whole_calls(extension): + """`fill` writes every element in one call, `edge` reads the first and last: with the list's + critical section held from copy-in to write-back, a reader sees a state between two whole calls, + so first == last, always.""" + r = run_child(extension, """ + shared = [0.0] * 20000 + torn = [] + stop = False + def writer(v): + def go(): + for i in range(40 * STRESS): + m.fill(shared, v + i) + return go + def reader(): + while not stop: + d = m.edge(shared) + if d != 0.0: + torn.append(d) + def driver(): + global stop + errs = run_threads([writer(1000.0 * k) for k in range(1, 4)]) + stop = True + return errs + rs = [threading.Thread(target=lambda: errs_r.extend(run_threads([reader] * 3)))] + errs_r = [] + rs[0].start() + errs = driver() + rs[0].join() + print(json.dumps({"torn": len(torn), "errs": errs + errs_r})) + """) + assert r == {"torn": 0, "errs": []} + + +def test_shared_list_int_writers_and_readers(extension): + r = run_child(extension, """ + shared = [0] * 20000 + torn = [] + stop = False + def writer(v): + def go(): + for i in range(40 * STRESS): + m.fill_int(shared, v + i) + return go + def reader(): + while not stop: + d = m.edge_int(shared) + if d != 0: + torn.append(d) + errs_r = [] + rt = threading.Thread(target=lambda: errs_r.extend(run_threads([reader] * 3))) + rt.start() + errs = run_threads([writer(1000 * k) for k in range(1, 4)]) + stop = True + rt.join() + print(json.dumps({"torn": len(torn), "errs": errs + errs_r})) + """) + assert r == {"torn": 0, "errs": []} + + +def test_shared_list_read_modify_write_equals_a_serialisation(extension): + """`rotate` is a read-modify-write of the whole list: T calls from any threads must leave the + list rotated by exactly T (a lost update or a torn copy would not).""" + r = run_child(extension, """ + n = 64 + shared = [float(i) for i in range(n)] + T_PER, THREADS = 150 * STRESS, 6 + errs = run_threads([(lambda: [m.rotate(shared) for _ in range(T_PER)]) + for _ in range(THREADS)]) + k = (T_PER * THREADS) % n + want = [float(i) for i in list(range(k, n)) + list(range(k))] + print(json.dumps({"ok": shared == want, "errs": errs})) + """) + assert r == {"ok": True, "errs": []} + + +def test_two_shared_lists(extension): + r = run_child(extension, """ + a = [7] * 3000 + b = [9] * 3000 + bad = [] + def zero(): + for _ in range(30 * STRESS): + m.zero2(a, b) + m.zero2(b, a) # the opposite order: the two-list section is ordered + a[:] = [7] * 3000 + b[:] = [9] * 3000 + def look(): + for _ in range(200 * STRESS): + d = m.pair_edge(a, b) + if d not in (7 - 9, 0, 7, -9): + bad.append(d) + errs = run_threads([zero, zero, look, look]) + print(json.dumps({"bad": bad, "errs": errs})) + """) + assert r == {"bad": [], "errs": []} + + +def test_shared_objects_concurrent_fieldset_and_fieldget(extension): + r = run_child(extension, """ + cell = m.Cell((0,), None) + written = {(k, i) for k in range(1, 7) for i in range(200 * STRESS)} | {(0,)} + bad = [] + def writer(k): + def go(): + for i in range(200 * STRESS): + m.seta(cell, (k, i)) + return go + def reader(): + for _ in range(2000 * STRESS): + v = m.geta(cell) + if v not in written: + bad.append(v) + def maker(): + for i in range(500 * STRESS): + c = m.mk((i,), [i]) + if c.a != (i,) or c.b != [i]: + bad.append(("mk", i)) + errs = run_threads([writer(k) for k in range(1, 4)] + [reader] * 3 + [maker] * 2) + print(json.dumps({"bad": [repr(x) for x in bad[:3]], "errs": errs})) + """) + assert r == {"bad": [], "errs": []} + + +def test_class_modified_while_threads_read_fields(extension): + r = run_child(extension, """ + cell = m.Cell(11, 22) + stop = False + bad = [] + started = threading.Barrier(5) # 4 readers + the modifier + def reader(): + started.wait() + while True: # at least one pass after the last modification + done = stop + if m.geta(cell) != 11: + bad.append("get") + c = m.mk(1, 2) + if (c.a, c.b) != (1, 2): + bad.append("mk") + m.seta(cell, 11) + if done: + break + def modifier(): + global stop + started.wait() # without this it can finish before any reader runs + for i in range(300 * STRESS): + m.Cell.counter = i # every assignment moves the class's version tag + if i % 7 == 0: + m.Cell.other = lambda self: i + stop = True + errs_r = [] + rt = threading.Thread(target=lambda: errs_r.extend(run_threads([reader] * 4))) + rt.start() + errs = run_threads([modifier]) + rt.join() + info = m.__typedpython_class_info__()["Cell"] + print(json.dumps({"bad": bad[:3], "errs": errs + errs_r, "compiled": info["compiled"], + "refreshed": info["refreshed"] > 0})) + """) + assert r == {"bad": [], "errs": [], "compiled": True, "refreshed": True} + + +def test_global_rebinding_with_the_dict_watcher(extension): + """The thread that rebinds `Cell` calls `mk` right after: it must see its own rebinding, however + many other threads are refreshing the global's cache at the same moment.""" + r = run_child(extension, """ + stop = False + bad = [] + def hammer(): + while not stop: + c = m.mk(1, 2) + if type(c).__name__ not in ("Cell", "Twin") or (c.a, c.b) != (1, 2): + bad.append("hammer") + errs_r = [] + ht = threading.Thread(target=lambda: errs_r.extend(run_threads([hammer] * 4))) + ht.start() + orig = m.Cell + for i in range(10000 * STRESS): + m.Cell = m.Twin if i % 2 == 0 else orig + want = "Twin" if i % 2 == 0 else "Cell" + got = type(m.mk(1, 2)).__name__ + if got != want: + bad.append((i, got, want)) + stop = True + ht.join() + print(json.dumps({"bad": bad[:3], "errs": errs_r})) + """) + assert r == {"bad": [], "errs": []} + + +def test_deopt_counter_does_not_lose_updates(extension): + r = run_child(extension, """ + N, M = 8, 300 * STRESS + before = m.__typedpython_deopts__ + res = [] + def go(): + for _ in range(M): + res.append(m.usek(1.5)) # a float for an int parameter: deopt, interpreted + errs = run_threads([go for _ in range(N)]) + print(json.dumps({"deopts": m.__typedpython_deopts__ - before, "want": N * M, + "same": set(res) == {4.5}, "errs": errs})) + """) + assert r["errs"] == [] and r["same"] + assert r["deopts"] == r["want"] + + +def test_entry_global_rebinding_in_another_thread(extension): + r = run_child(extension, """ + stop = False + bad = [] + def reader(): + while not stop: + v = m.usek(10) + if v not in (13, 110, 1010): + bad.append(v) + errs_r = [] + rt = threading.Thread(target=lambda: errs_r.extend(run_threads([reader] * 4))) + rt.start() + for i in range(3000 * STRESS): + m.K = (3, 100, 1000)[i % 3] + stop = True + rt.join() + print(json.dumps({"bad": bad[:3], "errs": errs_r})) + """) + assert r == {"bad": [], "errs": []} + + +# --- the mutation: without the list's critical section the shared-list test must fail ---------------- + +def _mutant(built, tmp: Path, drop: str) -> Path: + result, path, _ = built + c_text = cgen.generate(result.module, path) + kept = [ln for ln in c_text.split("\n") if drop not in ln] + assert len(kept) < len(c_text.split("\n")), f"{drop} not found in the generated C" + return cbuild.build("\n".join(kept), "ftmod", tmp / drop, ext_suffix=None) + + +@pytest.mark.race_mutant +def test_mutant_without_the_array_critical_section_is_caught(built, tmp_path): + so = _mutant(built, tmp_path, "tp_cs_begin") + detected = False + for _ in range(3): + try: + r = run_child(so, """ + shared = [0.0] * 20000 + torn = [] + stop = False + def writer(v): + def go(): + for i in range(60): + m.fill(shared, v + i) + return go + def reader(): + while not stop: + d = m.edge(shared) + if d != 0.0: + torn.append(d) + errs_r = [] + rt = threading.Thread(target=lambda: errs_r.extend(run_threads([reader] * 3))) + rt.start() + errs = run_threads([writer(1000.0 * k) for k in range(1, 4)]) + stop = True + rt.join() + print(json.dumps({"torn": len(torn)})) + """) + detected = r["torn"] > 0 + except AssertionError: + detected = True # a crash is a detection too + if detected: + break + assert detected, "the mutant (no critical section around copy-in) was not caught" + + +def _racy_runtime(tmp: Path, *, drop_dict_lock: bool, plain_epoch: bool) -> Path: + """A copy of tp_runtime.h with one #159 protection removed (the mutation of that protection).""" + h = (cbuild.RUNTIME_DIR / "tp_runtime.h").read_text() + g = h + if drop_dict_lock: + g = g.replace(" Py_BEGIN_CRITICAL_SECTION(module_dict);\n", "") + g = g.replace(" Py_END_CRITICAL_SECTION();\n if (rc <= 0) {\n return rc;", + " if (rc <= 0) {\n return rc;") + if plain_epoch: + g = g.replace("tp_atomic_add_u64(&tp_globals_epoch, 1);", "tp_globals_epoch++;") + g = g.replace("tp_atomic_load_u64(&tp_globals_epoch)", "tp_globals_epoch") + g = g.replace("tp_atomic_load_u64(&rt->gepoch)", "rt->gepoch") + g = g.replace("tp_atomic_store_u64(&rt->gepoch, epoch)", "rt->gepoch = epoch") + assert g != h + d = tmp / "rt" + d.mkdir(parents=True, exist_ok=True) + (d / "tp_runtime.h").write_text(g) + return d + + +@pytest.mark.race_mutant +def test_mutant_without_the_dict_critical_section_is_caught(built, tmp_path): + """Without the critical section around the epoch read and the global lookup, a thread can + remember a binding the rebinding thread has already replaced (the watcher fires before the store).""" + result, path, _ = built + rt = _racy_runtime(tmp_path, drop_dict_lock=True, plain_epoch=False) + so = cbuild.build(cgen.generate(result.module, path), "ftmod", tmp_path / "mut", runtime_dir=rt) + body = """ + stop = False + bad = [] + def hammer(): + while not stop: + m.mk(1, 2) + ht = threading.Thread(target=lambda: run_threads([hammer] * 6)) + ht.start() + orig = m.Cell + for i in range(20000): + m.Cell = m.Twin if i % 2 == 0 else orig + if type(m.mk(1, 2)).__name__ != ("Twin" if i % 2 == 0 else "Cell"): + bad.append(i) + stop = True + ht.join() + print(json.dumps({"stale": len(bad)})) + """ + stale = 0 + for _ in range(3): + stale = run_child(so, body)["stale"] + if stale: + break + assert stale > 0, "the mutant (no dict critical section) was not caught" + + +# --- ThreadSanitizer (best effort) -------------------------------------------------------------------- +# The interpreter is not instrumented, so TSan sees races between two accesses made by generated C or +# the header (the epoch, the class state) but NOT a missing list lock (the list is written by CPython). + +TSAN_SCRIPT = """ +cell = m.Cell(1, 2) +stop = False +def hammer(): + while not stop: + c = m.mk(1, 2); m.geta(cell); m.seta(cell, 5) +ht = threading.Thread(target=lambda: run_threads([hammer] * 3)); ht.start() +orig = m.Cell +for i in range(300): + m.Cell = m.Twin if i % 2 == 0 else orig + type(m.mk(1, 2)) + m.Cell.counter = i +stop = True; ht.join() +shared = [float(i) for i in range(500)] +run_threads([(lambda: [m.rotate(shared) for _ in range(50)]) for _ in range(4)]) +run_threads([(lambda: [m.fill(shared, 1.0) or m.total(shared, 2) for _ in range(50)]) for _ in range(4)]) +run_threads([(lambda: [m.usek(1.5) for _ in range(100)]) for _ in range(4)]) +print("TSAN-RUN-DONE") +""" + + +def _tsan_launcher(tmp: Path) -> Path | None: + if sys.platform != "darwin" and not sys.platform.startswith("linux"): + return None + cc = (sysconfig.get_config_var("CC") or "cc").split() + lc, launcher = tmp / "tsan_launcher.c", tmp / "tsan_python" + lc.write_text("#include \nint main(int argc, char **argv) { return Py_BytesMain(argc, argv); }\n") + libdir = sysconfig.get_config_var("LIBDIR") + link = subprocess.run(cc + ["-fsanitize=thread", "-g", f"-I{sysconfig.get_paths()['include']}", str(lc), + f"-L{libdir}", f"-lpython{sysconfig.get_config_var('LDVERSION')}", + f"-Wl,-rpath,{libdir}", "-o", str(launcher)], + capture_output=True, text=True) + return launcher if link.returncode == 0 else None + + +def _access_is_ours(block: str) -> bool: + """A report counts when one of its two racing accesses was made by the extension itself: the + first stack frame of that access outside the sanitizer runtime is in `ftmod` (generated C or the + header). A race whose accesses are inside libpython (seen on Linux CI: two `PyUnicode_FromString` + memcpys into memory the uninstrumented allocator had handed from one thread to another) is not + about this extension, even when an extension frame calls it further up.""" + for part in block.split("\n\n")[:2]: + frames = [ln for ln in part.splitlines() if ln.strip().startswith("#")] + first = next((f for f in frames if "libtsan" not in f and "sanitizer_common" not in f), "") + if "ftmod" in first: + return True + return False + + +def _tsan_reports(built, tmp: Path, launcher: Path, rt: Path | None) -> tuple[int, bool]: + result, path, _ = built + flags = list(cbuild.DEFAULT_FLAGS) + ["-O1", "-g", "-fno-omit-frame-pointer", "-fsanitize=thread"] + so = cbuild.build(cgen.generate(result.module, path), "ftmod", tmp / ("so-" + (rt.parent.name if rt else "real")), + runtime_dir=rt, flags=flags) + env = dict(os.environ, PYTHONHOME=sys.base_prefix, + TSAN_OPTIONS="halt_on_error=0:report_signal_unsafe=0:history_size=4") + env.pop("PYTHON_GIL", None) + env.pop("DYLD_INSERT_LIBRARIES", None) + run = subprocess.run([str(launcher), "-c", LOADER + textwrap.dedent(TSAN_SCRIPT), str(so), "1"], + capture_output=True, text=True, env=env, timeout=600) + out = run.stdout + run.stderr + # The interpreter is not instrumented, so TSan also reports races between two accesses inside + # CPython itself (seen on Linux CI). Count only reports with a frame of the extension or the + # header: those are the accesses this test is about. + blocks = out.split("WARNING: ThreadSanitizer")[1:] + ours = [b for b in blocks if _access_is_ours(b)] + _tsan_reports.first = ("WARNING: ThreadSanitizer" + ours[0])[:4000] if ours else "" + return len(ours), "TSAN-RUN-DONE" in out + + +@pytest.fixture(scope="module") +def tsan_launcher(tmp_path_factory): + d = tmp_path_factory.mktemp("tsan") + launcher = _tsan_launcher(d) + if launcher is None: + pytest.skip("cannot link a ThreadSanitizer launcher on this host") + return d, launcher + + +def test_threadsanitizer_reports_nothing_on_the_extension(built, tsan_launcher): + d, launcher = tsan_launcher + reports, done = _tsan_reports(built, d, launcher, None) + assert done and reports == 0, \ + f"{reports} ThreadSanitizer report(s) (done={done}); first:\n{_tsan_reports.first}" + + +@pytest.mark.race_mutant +def test_threadsanitizer_sees_a_plain_epoch(built, tsan_launcher): + """The check can fail: with the epoch back to plain loads and increments TSan reports the race + between the watcher callback and the readers.""" + d, launcher = tsan_launcher + rt = _racy_runtime(d / "racy", drop_dict_lock=True, plain_epoch=True) + reports, done = _tsan_reports(built, d, launcher, rt) + assert done and reports > 0 diff --git a/python-multiplatform-ksp/src/test/python/test_frontend.py b/python-multiplatform-ksp/src/test/python/test_frontend.py index f82cd9379..3a51a3556 100644 --- a/python-multiplatform-ksp/src/test/python/test_frontend.py +++ b/python-multiplatform-ksp/src/test/python/test_frontend.py @@ -116,7 +116,9 @@ def g(a: list[float], n: int) -> None: assert f.locals == {"m": OBJ} assert f.body[:2] == ( Assign("m", Box(OBJ, Local(I64, "n"))), - Assign("m", BinOp(OBJ, BinOpKind.ADD, Local(OBJ, "m"), Box(OBJ, Const(I64, 1)))), + # `m += 1` on an object is the in-place operation (PyNumber_InPlaceAdd, #150) + Assign("m", BinOp(OBJ, BinOpKind.ADD, Local(OBJ, "m"), Box(OBJ, Const(I64, 1)), + inplace=True)), ) assert not f.pure and not f.may_deopt @@ -185,10 +187,11 @@ def f(x: float) -> float: assert not any(n.proven for n in nodes(f, BinOp) + nodes(f, UnaryOp)) -@pytest.mark.parametrize("expr", ["i // 2", "i / 2"]) +@pytest.mark.parametrize("expr", ["i // 3", "i / 2"]) def test_int_division_cannot_be_recorded_as_proven_so_it_deopts(tmp_path, expr): - # ir.BinOp.proven exists for ADD/SUB/MUL only; the verifier cannot re-prove // and / on i64, - # so they count as deopting: in a pure function may_deopt, in an impure one a CPython int. + # ir.BinOp.proven exists for ADD/SUB/MUL and for // and % by a power of two of a value >= 0 + # (#148, test_interval_proofs.py); the verifier cannot re-prove // by 3 or / on i64, so they + # count as deopting: in a pure function may_deopt, in an impure one a CPython int. m = lower(tmp_path, f""" @compiled def f(n: int) -> float: @@ -320,14 +323,15 @@ def bump_last(a: list[float], e: float) -> None: def test_list_params_become_array_params_with_stored(tmp_path): m = lower(tmp_path, """ @compiled - def scale(a: list[float], b: list[float], k: float, c: list[int]) -> None: + def scale(a: list[float], b: list[float], k: float) -> None: for i in range(len(a)): a[i] = b[i] * k """) f = function(m, "scale") + # two list parameters at most (#159: the lists are locked from copy-in to write-back; CPython + # has no three-object critical section). A third one is test_free_threaded.py's. assert f.params == ( - ArrayParam("a", Type.F64_ARRAY, True), ArrayParam("b", Type.F64_ARRAY, False), - Param("k", F64), ArrayParam("c", Type.I64_ARRAY, False), + ArrayParam("a", Type.F64_ARRAY, True), ArrayParam("b", Type.F64_ARRAY, False), Param("k", F64), ) assert f.body[0].stop == Len(I64, "a") @@ -707,7 +711,8 @@ def render(n: int, scale: float) -> float: ExprStmt(CallObject(OBJ, Global(OBJ, "draw"), ( Local(OBJ, "canvas"), Box(OBJ, Local(F64, "x")), Box(OBJ, Const(F64, 0.5)), ), ("alpha",))), - Assign("hits", BinOp(OBJ, BinOpKind.ADD, Local(OBJ, "hits"), Box(OBJ, Const(I64, 1)))), + Assign("hits", BinOp(OBJ, BinOpKind.ADD, Local(OBJ, "hits"), Box(OBJ, Const(I64, 1)), + inplace=True)), # `hits += 1` on an object: in place (#150) If(CompareObj(BOOL, CompareKind.GT, Local(OBJ, "hits"), Box(OBJ, Local(I64, "n"))), (Break(),)), ) diff --git a/python-multiplatform-ksp/src/test/python/test_frontend_classes.py b/python-multiplatform-ksp/src/test/python/test_frontend_classes.py new file mode 100644 index 000000000..33c0473cc --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_frontend_classes.py @@ -0,0 +1,609 @@ +"""TypedPython front end: fixed-layout classes, None and identity (SPEC N-11, ir.py contract). + +A class is lowered to an `ir.ClassDecl` only when its layout is fixed in CPython too: the user wrote +`__slots__` equal to the annotated fields, no base but object, no metaclass, no decorator but +`@compiled`, and the module never subclasses it, assigns its class attributes or rebinds its name. +Every rule has an accepting and a rejecting test. A rejected class has no ClassDecl, an entry in +`Module.skipped` (keyed by the class name) with the reason, and functions using it lower as before +(`GetAttr` / `CallObject`). + +Contract choice (ir.ClassDecl says "`__init__(self, *fields-in-order)`"): `trivial_init` requires the +i-th `__init__` parameter to be stored into the i-th slot of `__slots__`; the statements may come in +any order and the parameter names are free. `ClassDecl.fields` is the `__slots__` order and +`New(cls, args)` takes the call's arguments in that order. +""" +import dataclasses +import textwrap +from pathlib import Path + +import pytest + +from typedpython import frontend +from typedpython.ir import ( + And, Assign, Box, Call, CallObject, CheckExact, ClassDecl, Const, ExprStmt, FieldGet, FieldSet, + GetAttr, Global, If, Is, Local, New, Or, Param, Return, Type, +) + +I64, F64, BOOL, OBJ, NONE = Type.I64, Type.F64, Type.BOOL, Type.OBJ, Type.NONE +BINARY_TREES = Path(__file__).resolve().parents[4] / "benchmarks/typedpython/py/binary_trees.py" +SLOTS_LINE = ' __slots__ = ("left", "right")\n' + +MARK = "# typedpython: compiled\n" + +POINT = """ +class P: + __slots__ = ("x", "y") + x: float + y: float + + def __init__(self, x: float, y: float) -> None: + self.x = x + self.y = y +""" + + +def lower(tmp_path, source, name="mod.py"): + path = tmp_path / name + path.write_text(MARK + textwrap.dedent(source).lstrip("\n")) + return frontend.lower(path) + + +def function(module, name): + found = [f for f in module.functions if f.name == name] + assert found, f"{name} was not lowered: {module.skipped.get(name)!r}" + return found[0] + + +def cls(module, name): + found = [c for c in module.classes if c.name == name] + assert found, f"{name} has no ClassDecl: {module.skipped.get(name)!r}" + return found[0] + + +def nodes(root, kind): + found, stack = [], [root] + while stack: + x = stack.pop() + if isinstance(x, (tuple, list)): + stack.extend(x) + elif dataclasses.is_dataclass(x) and not isinstance(x, type): + if isinstance(x, kind): + found.append(x) + stack.extend(getattr(x, f.name) for f in dataclasses.fields(x)) + return found + + +def rejected(tmp_path, source, name, reason): + m = lower(tmp_path, source) + assert all(c.name != name for c in m.classes), f"{name} should not be a ClassDecl" + assert reason in m.skipped[name], m.skipped[name] + return m + + +# --- ClassDecl: accepted --------------------------------------------------------------------------- + +def test_slots_class_in_marked_module_is_a_classdecl(tmp_path): + m = lower(tmp_path, POINT) + assert cls(m, "P") == ClassDecl("P", ("x", "y"), True) + assert "P" not in m.skipped + + +def test_decorated_class_in_unmarked_module(tmp_path): + path = tmp_path / "d.py" + path.write_text(textwrap.dedent(""" + @compiled + class P: + __slots__ = ["a"] + a: int + + def __init__(self, a: int) -> None: + self.a = a + + class Q: + __slots__ = ("b",) + b: int + """)) + m = frontend.lower(path) + assert [c.name for c in m.classes] == ["P"] # Q is neither decorated nor in a marked module + assert m.classes[0].fields == ("a",) + + +def test_fields_keep_the_slots_order_not_the_annotation_order(tmp_path): + m = lower(tmp_path, """ + class P: + __slots__ = ("y", "x") + x: int + y: int + def __init__(self, a: int, b: int) -> None: + self.y = a + self.x = b + """) + assert cls(m, "P") == ClassDecl("P", ("y", "x"), True) + + +def test_explicit_object_base_and_docstring_are_accepted(tmp_path): + m = lower(tmp_path, """ + class P(object): + "doc" + __slots__ = ("x",) + x: int + """) + assert cls(m, "P").fields == ("x",) + assert cls(m, "P").trivial_init is False # no __init__: object's, not an assign-each-slot one + + +def test_methods_are_allowed_and_stay_interpreted(tmp_path): + m = lower(tmp_path, POINT + """ + def norm(self) -> float: + return self.x +""") + assert cls(m, "P").fields == ("x", "y") + + +# --- ClassDecl: rejected --------------------------------------------------------------------------- + +def test_missing_slots_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + x: int + def __init__(self, x: int) -> None: + self.x = x + """, "P", "`__slots__`") + + +def test_slots_differing_from_annotations_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x", "z") + x: int + y: int + """, "P", "annotated fields") + + +def test_slots_not_a_literal_is_rejected(tmp_path): + rejected(tmp_path, """ + NAMES = ("x",) + class P: + __slots__ = NAMES + x: int + """, "P", "literal") + + +def test_duplicate_slot_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x", "x") + x: int + """, "P", "twice") + + +def test_base_class_is_rejected(tmp_path): + m = rejected(tmp_path, """ + class B: + pass + class P(B): + __slots__ = ("x",) + x: int + """, "P", "base") + assert all(c.name != "B" for c in m.classes) # B has no slots either + + +def test_metaclass_keyword_is_rejected(tmp_path): + rejected(tmp_path, """ + class P(metaclass=type): + __slots__ = ("x",) + x: int + """, "P", "metaclass") + + +def test_decorator_other_than_compiled_is_rejected(tmp_path): + rejected(tmp_path, """ + import dataclasses + @dataclasses.dataclass + class P: + __slots__ = ("x",) + x: int + """, "P", "decorator") + + +def test_subclass_in_the_module_rejects_the_base(tmp_path): + m = rejected(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + class Q(P): + __slots__ = () + """, "P", "subclass") + assert m.classes == () + + +def test_subclass_nested_in_a_function_still_counts(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + def make(): + class Q(P): + pass + return Q + """, "P", "subclass") + + +def test_class_attribute_assignment_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + P.count = 0 + """, "P", "class attribute") + + +def test_class_attribute_assignment_in_a_function_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + def f() -> None: + setattr(P, "count", 1) + """, "P", "class attribute") + + +def test_class_level_value_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + limit = 3 + """, "P", "class body") + + +def test_rebound_class_name_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + P = 3 + """, "P", "bound more than once") + + +def test_global_rebinding_in_a_function_is_rejected(tmp_path): + rejected(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + def f() -> None: + global P + P = None + """, "P", "bound more than once") + + +@pytest.mark.parametrize("dunder", ["__getattribute__", "__setattr__", "__getattr__", "__new__"]) +def test_slot_access_overrides_are_rejected(tmp_path, dunder): + # these run user code on a slot read/write/construct, which the direct slot access would skip + rejected(tmp_path, f""" + class P: + __slots__ = ("x",) + x: int + def {dunder}(self, *a): + return 0 + """, "P", dunder) + + +# --- trivial_init ------------------------------------------------------------------------------------ + +def test_init_with_free_parameter_names_and_statement_order_is_trivial(tmp_path): + m = lower(tmp_path, """ + class P: + __slots__ = ("x", "y") + x: int + y: int + def __init__(self, a: int, b: int) -> None: + self.y = b + self.x = a + """) + assert cls(m, "P").trivial_init is True + + +@pytest.mark.parametrize("body, why", [ + ("self.x = x", "assigns 1 of 2"), # a slot left unset + ("self.x = x\n self.y = y\n self.x = x", "more than once"), + ("self.x = x\n self.y = x + 1", "exactly"), # not a plain parameter + ("self.x = x\n self.y = y\n print(1)", "exactly"), # extra statement + ("self.x = y\n self.y = x", "order"), # params not in slot order +]) +def test_non_trivial_init_keeps_the_class_but_not_new(tmp_path, body, why): # `why` documents the case + m = lower(tmp_path, f""" + class P: + __slots__ = ("x", "y") + x: int + y: int + def __init__(self, x: int, y: int) -> None: + {body} + @compiled + def make(a: int) -> P: + return P(a, a) + """) + assert cls(m, "P") == ClassDecl("P", ("x", "y"), False) + f = function(m, "make") + assert nodes(f, New) == [] + assert f.body == (Return(CallObject(OBJ, Global(OBJ, "P"), ( + Box(OBJ, Local(I64, "a")), Box(OBJ, Local(I64, "a"))))),) + assert not f.pure + + +def test_init_with_defaults_or_extra_params_is_not_trivial(tmp_path): + m = lower(tmp_path, """ + class P: + __slots__ = ("x",) + x: int + def __init__(self, x: int = 0) -> None: + self.x = x + """) + assert cls(m, "P").trivial_init is False + + +# --- None and identity ------------------------------------------------------------------------------- + +def test_none_literal_is_an_obj_const(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def nothing() -> P | None: + return None +""") + f = function(m, "nothing") + assert f.body == (Return(Const(OBJ, None)),) + assert f.pure and not f.may_deopt + + +def test_is_none_and_is_not_none_and_identity(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(a: P | None, b: P | None) -> int: + if a is None: + return 0 + if a is not b: + return 1 + return 2 +""") + f = function(m, "f") + none = Const(OBJ, None) + assert f.body[0] == If(Is(BOOL, Local(OBJ, "a"), none), (Return(Const(I64, 0)),)) + assert f.body[1] == If(Is(BOOL, Local(OBJ, "a"), Local(OBJ, "b"), True), (Return(Const(I64, 1)),)) + assert f.pure and not f.may_deopt + + +def test_is_in_and_or_conditions(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(a: P | None, b: P | None) -> int: + if a is None or b is None: + return 0 + return 1 +""") + f = function(m, "f") + none = Const(OBJ, None) + assert f.body[0] == If(Or(BOOL, Is(BOOL, Local(OBJ, "a"), none), Is(BOOL, Local(OBJ, "b"), none)), + (Return(Const(I64, 0)),)) + + +def test_identity_of_a_scalar_is_not_lowered(tmp_path): + m = lower(tmp_path, """ + @compiled + def f(a: int) -> bool: + return a is None + """) + assert "f" not in [fn.name for fn in m.functions] + assert "identity" in m.skipped["f"] + + +# --- parameters and locals ----------------------------------------------------------------------------- + +def test_class_parameters_are_obj_with_cls_and_optional(tmp_path): + m = lower(tmp_path, "from typing import Optional\n" + POINT + """ +@compiled +def f(a: P, b: P | None, c: "P | None", d: Optional[P], e: None | P) -> int: + return 1 +""") + f = function(m, "f") + assert f.params == ( + Param("a", OBJ, "P", False), Param("b", OBJ, "P", True), Param("c", OBJ, "P", True), + Param("d", OBJ, "P", True), Param("e", OBJ, "P", True)) + + +def test_annotation_naming_a_rejected_class_is_a_plain_object(tmp_path): + m = lower(tmp_path, """ + class P: + x: int + @compiled + def f(a: P) -> int: + return 1 + """) + assert function(m, "f").params == (Param("a", OBJ),) + + +# --- field reads ----------------------------------------------------------------------------------------- + +def test_field_read_of_a_non_optional_parameter_is_proved(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(p: P) -> object: + return p.x +""") + f = function(m, "f") + assert f.body == (Return(FieldGet(OBJ, Local(OBJ, "p"), "P", "x")),) + assert f.pure and not f.may_deopt # a slot read is not an effect, nothing deopts + assert nodes(f, CheckExact) == [] and nodes(f, GetAttr) == [] + + +def test_field_read_of_an_optional_parameter_is_checked_in_a_pure_function(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(p: P | None) -> object: + return p.x +""") + f = function(m, "f") + assert f.body == (Return(FieldGet(OBJ, CheckExact(OBJ, Local(OBJ, "p"), "P"), "P", "x")),) + assert f.pure and f.may_deopt + + +def test_field_read_after_a_rebinding_is_not_trusted(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(p: P, q: P | None) -> object: + p = q + return p.x +""") + f = function(m, "f") + assert len(nodes(f, CheckExact)) == 1 and f.may_deopt + + +def test_unproved_field_read_in_an_impure_function_is_skipped(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(p: P | None) -> object: + print(1) + return p.x +""") + assert "f" not in [fn.name for fn in m.functions] + assert "not proved" in m.skipped["f"] + + +def test_proved_field_read_in_an_impure_function_is_lowered(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(p: P) -> object: + print(1) + return p.x +""") + f = function(m, "f") + assert nodes(f, FieldGet) == [FieldGet(OBJ, Local(OBJ, "p"), "P", "x")] + assert not f.pure and not f.may_deopt + + +def test_attribute_that_is_not_a_field_stays_getattr(tmp_path): + m = lower(tmp_path, POINT + """ + def norm(self) -> float: + return self.x + +""" + """ +@compiled +def f(p: P) -> object: + return p.norm +""") + f = function(m, "f") + assert nodes(f, GetAttr) and nodes(f, FieldGet) == [] + assert not f.pure + + +def test_fresh_new_local_is_proved_exact(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(a: float) -> object: + p = P(a, a) + return p.y +""") + f = function(m, "f") + assert nodes(f, FieldGet) == [FieldGet(OBJ, Local(OBJ, "p"), "P", "y")] + assert nodes(f, CheckExact) == [] and f.pure and not f.may_deopt + + +# --- field writes ------------------------------------------------------------------------------------------ + +def test_field_write_is_an_effect(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(p: P, v: float) -> None: + p.x = v +""") + f = function(m, "f") + assert f.body == (FieldSet(Local(OBJ, "p"), "P", "x", Box(OBJ, Local(F64, "v"))),) + assert not f.pure and not f.may_deopt + + +def test_field_write_on_an_unproved_object_is_skipped(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(p: P | None, v: float) -> None: + p.x = v +""") + assert "f" not in [fn.name for fn in m.functions] + assert "not proved" in m.skipped["f"] + + +def test_write_to_an_attribute_of_an_unknown_object_is_still_skipped(tmp_path): + m = lower(tmp_path, """ + @compiled + def f(p: object) -> None: + p.x = 1 + """) + assert "attribute" in m.skipped["f"] + + +# --- construction ------------------------------------------------------------------------------------------- + +def test_construction_with_a_trivial_init_is_new_in_field_order(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(a: float, b: float) -> object: + return P(a, b) +""") + f = function(m, "f") + assert f.body == (Return(New(OBJ, "P", (Box(OBJ, Local(F64, "a")), Box(OBJ, Local(F64, "b"))))),) + assert f.pure and not f.may_deopt # allocating is not an effect + + +def test_construction_with_wrong_arity_or_keywords_is_a_plain_call(tmp_path): + m = lower(tmp_path, POINT + """ +@compiled +def f(a: float) -> object: + return P(a) + +@compiled +def g(a: float) -> object: + return P(x=a, y=a) +""") + for name in ("f", "g"): + assert nodes(function(m, name), New) == [] + assert len(nodes(function(m, name), CallObject)) == 1 + + +# --- the binary-trees benchmark ------------------------------------------------------------------------------ + +def test_binary_trees_with_slots_lowers_make_and_check(tmp_path): + source = BINARY_TREES.read_text() + assert "class Node:\n" + SLOTS_LINE in source # the benchmark itself declares the slots + path = tmp_path / "binary_trees.py" + path.write_text(MARK + source) + m = frontend.lower(path) + + assert cls(m, "Node") == ClassDecl("Node", ("left", "right"), True) + make, check = function(m, "make"), function(m, "check") + assert make.pure and check.pure + assert make.params == (Param("depth", I64),) + assert check.params == (Param("node", OBJ, "Node", False),) + assert len(nodes(make, New)) == 2 + assert sorted(nodes(check, FieldGet), key=lambda g: g.field) == [ + FieldGet(OBJ, Local(OBJ, "node"), "Node", "left"), + FieldGet(OBJ, Local(OBJ, "node"), "Node", "right")] + assert nodes(check, Is) + assert nodes(check, CallObject) == [] and nodes(check, GetAttr) == [] + # report: check() may deopt (i64 arithmetic, and `check(left)` passes an `Node | None` local to + # a parameter guarded `type(x) is Node`); make() no longer does (#148: `depth - 1` sits under + # `if depth > 0`, so it cannot overflow) + print(f"make.may_deopt={make.may_deopt} check.may_deopt={check.may_deopt}") + assert check.may_deopt and not make.may_deopt + + +def test_binary_trees_without_slots_stays_object_code(tmp_path): + path = tmp_path / "binary_trees.py" + source = BINARY_TREES.read_text() + assert SLOTS_LINE in source + path.write_text(MARK + source.replace(SLOTS_LINE, "", 1)) # the run.py no-slots baseline + m = frontend.lower(path) + assert m.classes == () + assert "`__slots__`" in m.skipped["Node"] + # as before: object code. `check` reads attributes (impure), so its int arithmetic would be + # CPython ints and it cannot return a native int: it stays interpreted; `make` calls `Node`. + assert "check" not in [f.name for f in m.functions] + make = function(m, "make") + assert nodes(make, New) == [] and nodes(make, CallObject) and not make.pure diff --git a/python-multiplatform-ksp/src/test/python/test_gate_bundle.py b/python-multiplatform-ksp/src/test/python/test_gate_bundle.py new file mode 100644 index 000000000..985072bf5 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_gate_bundle.py @@ -0,0 +1,92 @@ +"""The TypedPython gate bundle published beside the KSP processor (#20, toolchain#90). + +toolchain's typedpythonCheck resolves `python-multiplatform-ksp::typedpython-gate@zip`, +unzips it and installs the gate from it with uv `--require-hashes`, so the zip must hold the wheel +under its real file name and a manifest with every hash the install needs. +""" +import hashlib +import json +import subprocess +import sys +import tomllib +import zipfile +from pathlib import Path + +import pytest + +MODULE = Path(__file__).resolve().parents[3] # python-multiplatform-ksp +SCRIPT = MODULE / "gate_bundle.py" +LOCK = MODULE / "typedpython-gate.lock.json" + + +@pytest.fixture(scope="module") +def bundle(tmp_path_factory): + out = tmp_path_factory.mktemp("gate") / "typedpython-gate.zip" + run = subprocess.run([sys.executable, str(SCRIPT), "--out", str(out)], capture_output=True, text=True) + assert run.returncode == 0, run.stdout + run.stderr + return out + + +def _pyproject(): + return tomllib.loads((MODULE / "pyproject.toml").read_text()) + + +def test_zip_holds_the_wheel_under_its_real_name_and_a_manifest(bundle): + names = zipfile.ZipFile(bundle).namelist() + version = _pyproject()["project"]["version"] + assert sorted(names) == sorted([f"typedpython-{version}-py3-none-any.whl", "typedpython-gate.json"]) + + +def test_manifest_matches_the_wheel_and_the_pins(bundle): + z = zipfile.ZipFile(bundle) + m = json.loads(z.read("typedpython-gate.json")) + project = _pyproject()["project"] + assert m["format"] == 1 + assert m["typedpython"]["version"] == project["version"] + wheel = m["typedpython"]["wheel"] + assert m["typedpython"]["sha256"] == hashlib.sha256(z.read(wheel)).hexdigest() + # the pyrefly pin is the one the wheel itself depends on, with a hash for every published wheel + assert f"pyrefly=={m['pyrefly']['version']}" in project["dependencies"] + assert m["pyrefly"]["sha256"] == json.loads(LOCK.read_text())["pyrefly"]["sha256"] + assert any("macosx_11_0_arm64" in f for f in m["pyrefly"]["sha256"]) + assert all(f.endswith(".whl") for f in m["pyrefly"]["sha256"]) + + +def test_lock_pins_the_same_pyrefly_as_the_package(): + lock = json.loads(LOCK.read_text())["pyrefly"] + assert f"pyrefly=={lock['version']}" in _pyproject()["project"]["dependencies"] + assert all(f.startswith(f"pyrefly-{lock['version']}-") for f in lock["sha256"]) + + +def test_the_wheel_installs_with_hashes_and_checks_code(bundle, tmp_path): + # the install toolchain does, minus the network: typedpython from the unzipped dir with its hash + # (--no-deps here: pyrefly comes from PyPI there, from this test's own interpreter here) + d = tmp_path / "unzipped" + zipfile.ZipFile(bundle).extractall(d) + m = json.loads((d / "typedpython-gate.json").read_text()) + req = tmp_path / "req.txt" + req.write_text(f"typedpython=={m['typedpython']['version']} --hash=sha256:{m['typedpython']['sha256']}\n") + target = tmp_path / "site" + run = subprocess.run(["uv", "pip", "install", "--python", sys.executable, "--target", str(target), + "--no-index", "--find-links", str(d), "--no-deps", "--require-hashes", + "-r", str(req)], capture_output=True, text=True) + assert run.returncode == 0, run.stdout + run.stderr + assert (target / "typedpython" / "__main__.py").is_file() + clean = tmp_path / "clean.py" + clean.write_text("def f(x: int) -> int:\n return x + 1\n") + env = {"PYTHONPATH": str(target), "PATH": __import__("os").environ["PATH"], + "TMPDIR": str(tmp_path)} + check = subprocess.run([sys.executable, "-m", "typedpython", "check", str(clean)], + capture_output=True, text=True, env=env) + assert check.returncode == 0, check.stdout + check.stderr + + +def test_the_wheel_carries_the_c_runtime_and_the_builtin_stub(bundle): + # compiling needs tp_runtime.h; checking needs the `compiled` builtin stub + import io + z = zipfile.ZipFile(bundle) + wheel = json.loads(z.read("typedpython-gate.json"))["typedpython"]["wheel"] + names = zipfile.ZipFile(io.BytesIO(z.read(wheel))).namelist() + for needed in ("typedpython/runtime/tp_runtime.h", "typedpython/builtin_stubs/__builtins__.pyi", + "typedpython_builtins.pth", "typedpython/__main__.py"): + assert needed in names, needed diff --git a/python-multiplatform-ksp/src/test/python/test_importgate.py b/python-multiplatform-ksp/src/test/python/test_importgate.py new file mode 100644 index 000000000..5db3fe144 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_importgate.py @@ -0,0 +1,146 @@ +"""Import-time type gate (SPEC N-7b, #183): development-mode import hook.""" +from __future__ import annotations + +import importlib +import sys + +import pytest + +from typedpython import gate, importgate + +BAD = """\ +from pathlib import Path +Path(__file__).with_name("marker.txt").write_text("ran") + +def f(x: int) -> int: + return x + +f("not an int") +""" + +GOOD = "VALUE: int = 1\n" + + +@pytest.fixture +def project(tmp_path, monkeypatch): + """A fresh source root on sys.path; the hook and imported modules are cleaned up afterwards.""" + root = tmp_path / "src" + root.mkdir() + monkeypatch.syspath_prepend(str(root)) + before = set(sys.modules) + yield root + importgate.uninstall() + for name in set(sys.modules) - before: + del sys.modules[name] + importlib.invalidate_caches() + + +@pytest.fixture +def calls(monkeypatch): + """Paths handed to the checker, one list per checker invocation (the seam).""" + seen: list[list[str]] = [] + real = importgate._run_gate + + def counting(paths, search_paths): + seen.append([str(p) for p in paths]) + return real(paths, search_paths) + + monkeypatch.setattr(importgate, "_run_gate", counting) + return seen + + +def test_type_error_stops_import_before_module_code_runs(project): + (project / "bad_mod.py").write_text(BAD) + importgate.install([project]) + with pytest.raises(ImportError) as info: + importlib.import_module("bad_mod") + assert isinstance(info.value, importgate.ImportGateError) + assert info.value.diagnostics + assert all(d.severity == "error" for d in info.value.diagnostics) + assert "bad_mod.py" in str(info.value) + assert not (project / "marker.txt").exists() + assert "bad_mod" not in sys.modules + + +def test_same_module_imports_without_the_hook(project): + (project / "bad_mod2.py").write_text(BAD) + importlib.import_module("bad_mod2") + assert (project / "marker.txt").exists() + + +def test_uninstall_restores_normal_import(project): + (project / "bad_mod3.py").write_text(BAD) + importgate.install([project]) + importgate.uninstall() + importlib.import_module("bad_mod3") + assert (project / "marker.txt").exists() + + +def test_clean_module_imports_and_is_cached_by_content(project, calls): + path = project / "good_mod.py" + path.write_text(GOOD) + importgate.install([project]) + mod = importlib.import_module("good_mod") + assert mod.VALUE == 1 + assert len(calls) == 1 + + del sys.modules["good_mod"] + importlib.import_module("good_mod") + assert len(calls) == 1, "an unchanged module must not be checked again" + + del sys.modules["good_mod"] + # a different length: CPython's .pyc check compares mtime in whole seconds and the size, so a + # same-size rewrite within one second would run the stale bytecode (CPython's own behaviour) + path.write_text("VALUE: int = 22\n") + assert importlib.import_module("good_mod").VALUE == 22 + assert len(calls) == 2, "changed content must be re-checked" + + +def test_failing_result_is_cached_too(project, calls): + (project / "bad_mod4.py").write_text(BAD) + importgate.install([project]) + for _ in range(2): + with pytest.raises(importgate.ImportGateError): + importlib.import_module("bad_mod4") + assert len(calls) == 1 + + +def test_modules_outside_source_roots_and_stdlib_are_not_checked(project, tmp_path, monkeypatch, calls): + other = tmp_path / "deps" + other.mkdir() + (other / "dep_mod.py").write_text(BAD.replace("marker.txt", "dep_marker.txt")) + monkeypatch.syspath_prepend(str(other)) + importgate.install([project]) + importlib.import_module("dep_mod") # a type error, but outside the roots + importlib.import_module("colorsys") # stdlib + assert calls == [] + assert (other / "dep_marker.txt").exists() + + +def test_package_submodule_under_root_is_checked(project, calls): + pkg = project / "pkg" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "sub.py").write_text("def f(x):\n return x\n") + importgate.install([project]) + with pytest.raises(importgate.ImportGateError): + importlib.import_module("pkg.sub") + assert any(p.endswith("sub.py") for c in calls for p in c) + + +def test_install_twice_installs_one_finder(project): + importgate.install([project]) + importgate.install([project]) + assert sum(isinstance(f, importgate.ImportGateFinder) for f in sys.meta_path) == 1 + importgate.uninstall() + assert not any(isinstance(f, importgate.ImportGateFinder) for f in sys.meta_path) + + +def test_install_from_env(project, monkeypatch): + monkeypatch.delenv(importgate.ENV_VAR, raising=False) + assert importgate.install_from_env() is False + monkeypatch.setenv(importgate.ENV_VAR, str(project)) + assert importgate.install_from_env() is True + (project / "bad_mod5.py").write_text(BAD) + with pytest.raises(importgate.ImportGateError): + importlib.import_module("bad_mod5") diff --git a/python-multiplatform-ksp/src/test/python/test_interval_proofs.py b/python-multiplatform-ksp/src/test/python/test_interval_proofs.py new file mode 100644 index 000000000..045a593e3 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_interval_proofs.py @@ -0,0 +1,586 @@ +"""Interval proofs that drop provably safe checked i64 operations (issue #148, SPEC N-8 / N-9). + +New facts, all re-proved by the verifier (the front end only proposes `proven`): + + * guards: inside `if x < e:` / `while lo < hi:` (and after a guard that returns) a local's + interval is narrowed by the comparison, until the local is assigned; + * locals: a local assigned only i64 values has the join of their intervals (a flow-insensitive + fixpoint with widening), so `x = 1; x + 1` and non-negative accumulators are known; + * `//` and `%` by a constant have bounded results; + * `//` and `%` by a positive power-of-two Const of a value in [0, 2**63-1] carry `proven` and + are emitted as a shift / mask (no tp_floordiv_i64 / tp_mod_i64); + * the front end's `len()` bound is 2**62, as the verifier's. + +Section 1 builds IR by hand (no front end): each fact is accepted when sound and rejected with the +PROVEN rule when the IR claims it without the fact. Section 2 runs source through the front end, +verifier, C back end and the C compiler, and compares the compiled function with CPython at the +boundary values. Builds go to pytest's basetemp inside the worktree's `.tmp/`. +""" +from __future__ import annotations + +import dataclasses +import importlib.machinery +import importlib.util +import re +from pathlib import Path + +import pytest + +from typedpython import cbuild, cgen, frontend, verify +from typedpython.ir import ( + And, ArrayParam, Assign, BinOp, BinOpKind, Compare, CompareKind, Const, ForRange, If, Index, + Local, Or, Param, Return, Type, UnaryOp, UnaryOpKind, While, +) +from test_verify import ( + IMPURE, PROVEN, FLAG_DEOPT, assert_accepted, assert_rejected, fn, i, kept, lb, li, padd, pmul, + psub, run, +) + +I64, BOOL = Type.I64, Type.BOOL +MAX, MIN = 2 ** 63 - 1, -2 ** 63 +A = [ArrayParam("a", Type.I64_ARRAY, stored=False)] +N = [Param("n", I64)] + + +def cmp(kind, l, r): return Compare(BOOL, kind, l, r) +def lt(l, r): return cmp(CompareKind.LT, l, r) +def le(l, r): return cmp(CompareKind.LE, l, r) +def gt(l, r): return cmp(CompareKind.GT, l, r) +def ge(l, r): return cmp(CompareKind.GE, l, r) +def eq(l, r): return cmp(CompareKind.EQ, l, r) +def pdiv(l, r): return BinOp(I64, BinOpKind.FLOORDIV, l, r, proven=True) +def pmod(l, r): return BinOp(I64, BinOpKind.MOD, l, r, proven=True) +def div(l, r): return BinOp(I64, BinOpKind.FLOORDIV, l, r) +def mod(l, r): return BinOp(I64, BinOpKind.MOD, l, r) +def elem(k=0): return Index(I64, "a", i(k)) + + +def pure_fn(body, params=N, locals_=None, may_deopt=False, **kw): + return fn(params=params, locals_=locals_, returns=I64, pure=True, may_deopt=may_deopt, + body=body, **kw) + + +# --- 1a. guards ---------------------------------------------------------------------------------- + +def test_a_less_than_guard_bounds_the_local_from_above(): + ok = pure_fn([If(lt(li("n"), i(MAX)), (Return(padd(li("n"), i(1))),)), Return(i(0))]) + assert_accepted(ok) + # `<=` leaves n == MAX possible + assert_rejected(PROVEN, pure_fn([If(le(li("n"), i(MAX)), (Return(padd(li("n"), i(1))),)), + Return(i(0))])) + # no guard at all + assert_rejected(PROVEN, pure_fn([Return(padd(li("n"), i(1)))])) + + +def test_a_greater_than_guard_bounds_the_local_from_below(): + assert_accepted(pure_fn([If(gt(li("n"), i(MIN)), (Return(psub(li("n"), i(1))),)), Return(i(0))])) + assert_rejected(PROVEN, pure_fn([If(ge(li("n"), i(MIN)), + (Return(psub(li("n"), i(1))),)), Return(i(0))])) + + +def test_the_local_may_be_on_the_right_of_the_comparison(): + assert_accepted(pure_fn([If(gt(i(MAX), li("n")), (Return(padd(li("n"), i(1))),)), + Return(i(0))])) + + +def test_the_other_side_may_be_any_expression_with_a_known_interval(): + # n < m, and m <= 2**63 - 1: n <= 2**63 - 2 + ps = [Param("n", I64), Param("m", I64)] + assert_accepted(pure_fn([If(lt(li("n"), li("m")), (Return(padd(li("n"), i(1))),)), + Return(i(0))], params=ps)) + # but n <= m only gives n <= 2**63 - 1 + assert_rejected(PROVEN, pure_fn([If(le(li("n"), li("m")), (Return(padd(li("n"), i(1))),)), + Return(i(0))], params=ps)) + + +def test_a_conjunction_applies_every_link_and_a_disjunction_none(): + both = And(BOOL, lt(li("n"), i(MAX)), gt(li("n"), i(MIN))) + assert_accepted(pure_fn([If(both, (Return(padd(psub(li("n"), i(1)), i(1))),)), Return(i(0))])) + either = Or(BOOL, lt(li("n"), i(MAX)), lt(li("n"), i(5))) + assert_rejected(PROVEN, pure_fn([If(either, (Return(padd(li("n"), i(1))),)), Return(i(0))])) + # the else branch of a disjunction has both links false + neither = Or(BOOL, ge(li("n"), i(MAX)), lt(li("n"), i(MIN + 1))) + assert_accepted(pure_fn([If(neither, (Return(i(0)),), (Return(padd(li("n"), i(1))),))])) + + +def test_not_and_the_else_branch_negate_the_condition(): + assert_accepted(pure_fn([If(UnaryOp(BOOL, UnaryOpKind.NOT, ge(li("n"), i(MAX))), + (Return(padd(li("n"), i(1))),)), Return(i(0))])) + assert_accepted(pure_fn([If(ge(li("n"), i(MAX)), (Return(i(0)),), + (Return(padd(li("n"), i(1))),))])) + + +def test_a_guard_that_returns_narrows_the_code_after_it(): + assert_accepted(pure_fn([If(ge(li("n"), i(MAX)), (Return(i(0)),)), + Return(padd(li("n"), i(1)))])) + # the branch falls through: the code after the If is reachable with n == MAX + assert_rejected(PROVEN, pure_fn([If(ge(li("n"), i(MAX)), (Assign("x", i(1)),)), + Return(padd(li("n"), i(1)))], locals_={"x": I64})) + + +def test_an_equality_guard_pins_the_local(): + assert_accepted(pure_fn([If(eq(li("n"), i(5)), (Return(padd(li("n"), i(MAX - 5))),)), + Return(i(0))])) + assert_rejected(PROVEN, pure_fn([If(eq(li("n"), i(5)), (Return(padd(li("n"), i(MAX - 4))),)), + Return(i(0))])) + + +def test_assigning_the_local_ends_the_guard(): + ps = N + A + ok = pure_fn([Assign("x", li("n")), + If(lt(li("x"), i(MAX)), (Return(padd(li("x"), i(1))),)), Return(i(0))], + params=ps, locals_={"x": I64}) + assert_accepted(ok) + killed = pure_fn([Assign("x", li("n")), + If(lt(li("x"), i(MAX)), (Assign("x", elem()), Return(padd(li("x"), i(1))))), + Return(i(0))], params=ps, locals_={"x": I64}) + assert_rejected(PROVEN, killed) + + +def test_a_while_condition_holds_at_the_top_of_each_iteration_until_the_local_is_assigned(): + # while lo < hi: lo += 1; hi -= 1 (the fannkuch reversal) + loop = While(lt(li("lo"), li("hi")), (Assign("lo", padd(li("lo"), i(1))), + Assign("hi", psub(li("hi"), i(1))))) + body = [Assign("lo", elem(0)), Assign("hi", elem(1)), loop, Return(i(0))] + assert_accepted(pure_fn(body, params=A, locals_={"lo": I64, "hi": I64})) + # lo <= hi lets lo be MAX (and hi be MIN): neither step is safe + weak = While(le(li("lo"), li("hi")), (Assign("lo", padd(li("lo"), i(1))),)) + assert_rejected(PROVEN, pure_fn([Assign("lo", elem(0)), Assign("hi", elem(1)), weak, Return(i(0))], + params=A, locals_={"lo": I64, "hi": I64})) + weak2 = While(le(li("lo"), li("hi")), (Assign("hi", psub(li("hi"), i(1))),)) + assert_rejected(PROVEN, pure_fn([Assign("lo", elem(0)), Assign("hi", elem(1)), weak2, Return(i(0))], + params=A, locals_={"lo": I64, "hi": I64})) + + +def test_a_guard_does_not_survive_the_back_edge_of_a_loop_that_assigns_the_local(): + # if x < MAX: while c: y = x + 1; x = a[0] second iteration: x is any i64 + inner = While(lb("c"), (Assign("y", padd(li("x"), i(1))), Assign("x", elem()))) + body = [Assign("x", li("n")), If(lt(li("x"), i(MAX)), (inner,)), Return(i(0))] + ps = N + A + [Param("c", BOOL)] + assert_rejected(PROVEN, pure_fn(body, params=ps, locals_={"x": I64, "y": I64})) + # the same loop with the guard as its own condition re-checks it every iteration + again = While(lt(li("x"), i(MAX)), (Assign("y", padd(li("x"), i(1))), Assign("x", elem()))) + assert_accepted(pure_fn([Assign("x", li("n")), again, Return(i(0))], params=N + A, + locals_={"x": I64, "y": I64})) + + +def test_a_guard_does_not_outlive_a_loop_that_assigns_the_local(): + inner = While(lb("c"), (Assign("x", elem()),)) + body = [Assign("x", li("n")), + If(lt(li("x"), i(MAX)), (inner, Return(padd(li("x"), i(1))))), Return(i(0))] + ps = N + A + [Param("c", BOOL)] + assert_rejected(PROVEN, pure_fn(body, params=ps, locals_={"x": I64})) + + +def test_a_guard_on_a_float_comparison_proves_nothing(): + ps = [Param("n", I64), Param("x", Type.F64)] + body = [If(lt(Const(Type.F64, 0.0), Local(Type.F64, "x")), (Return(padd(li("n"), i(1))),)), Return(i(0))] + assert_rejected(PROVEN, pure_fn(body, params=ps)) + + +# --- 1b. locals ---------------------------------------------------------------------------------- + +def test_a_local_assigned_a_constant_has_that_interval(): + body = [Assign("x", i(1)), Return(padd(li("x"), i(1)))] + assert_accepted(pure_fn(body, params=(), locals_={"x": I64})) + + +def test_a_local_has_the_join_of_all_its_assignments(): + ps = [Param("c", BOOL)] + def f(extra): + return pure_fn([If(lb("c"), (Assign("x", i(5)),), (Assign("x", i(7)),)), + Return(padd(li("x"), i(extra)))], params=ps, locals_={"x": I64}) + assert_accepted(f(MAX - 7)) + assert_rejected(PROVEN, f(MAX - 6)) + + +def test_a_local_assigned_from_an_unknown_value_is_full_range(): + body = [Assign("x", li("n")), Return(padd(li("x"), i(1)))] + assert_rejected(PROVEN, pure_fn(body, locals_={"x": I64})) + body = [Assign("x", i(0)), If(gt(li("n"), i(0)), (Assign("x", elem()),)), + Return(padd(li("x"), i(1)))] + assert_rejected(PROVEN, pure_fn(body, params=N + A, locals_={"x": I64})) + + +def test_a_counter_that_only_grows_is_non_negative(): + # x = 0; while c: x = x + 1 (checked) x in [0, MAX] + loop = While(lb("c"), (Assign("x", BinOp(I64, BinOpKind.ADD, li("x"), i(1))),)) + ps = [Param("c", BOOL)] + base = [Assign("x", i(0)), loop] + assert_accepted(pure_fn(base + [Return(pdiv(li("x"), i(4)))], params=ps, locals_={"x": I64}, + may_deopt=True)) + # a proven +1 on it is not provable: x can be MAX + assert_rejected(PROVEN, pure_fn(base + [Return(padd(li("x"), i(1)))], params=ps, + locals_={"x": I64}, may_deopt=True)) + + +def test_a_counter_that_only_shrinks_is_not_non_negative(): + loop = While(lb("c"), (Assign("x", BinOp(I64, BinOpKind.SUB, li("x"), i(1))),)) + ps = [Param("c", BOOL)] + assert_rejected(PROVEN, pure_fn([Assign("x", i(0)), loop, Return(pdiv(li("x"), i(4)))], + params=ps, locals_={"x": I64}, may_deopt=True)) + + +def test_the_fixpoint_terminates_and_stays_sound_for_mutually_dependent_locals(): + # x = y + 1 and y = x + 1 (checked): both start at 0 and grow without bound + loop = While(lb("c"), (Assign("x", BinOp(I64, BinOpKind.ADD, li("y"), i(1))), + Assign("y", BinOp(I64, BinOpKind.ADD, li("x"), i(1))))) + ps = [Param("c", BOOL)] + base = [Assign("x", i(0)), Assign("y", i(0)), loop] + loc = {"x": I64, "y": I64} + assert_accepted(pure_fn(base + [Return(pdiv(li("y"), i(2)))], params=ps, locals_=loc, + may_deopt=True)) + assert_rejected(PROVEN, pure_fn(base + [Return(padd(li("y"), i(1)))], params=ps, locals_=loc, + may_deopt=True)) + + +def test_a_for_range_variable_is_still_only_known_inside_its_loop(): + body = [Assign("k", i(0)), ForRange("k", i(0), i(5), i(1), ()), Return(padd(li("k"), i(1)))] + assert_rejected(PROVEN, pure_fn(body, params=(), locals_={"k": I64})) + + +def test_a_parameter_that_is_reassigned_is_still_unknown(): + body = [Assign("n", i(0)), Return(padd(li("n"), i(1)))] + assert_rejected(PROVEN, pure_fn(body)) + + +# --- 1c. // and % --------------------------------------------------------------------------------- + +def test_mod_by_a_positive_constant_is_between_zero_and_the_divisor_minus_one(): + ok = lambda extra: pure_fn([Return(padd(mod(li("n"), i(7)), i(extra)))], may_deopt=True) + assert_accepted(ok(MAX - 6)) + assert_rejected(PROVEN, ok(MAX - 5)) + + +def test_mod_by_a_negative_constant_is_between_the_divisor_plus_one_and_zero(): + assert_accepted(pure_fn([Return(padd(mod(li("n"), i(-7)), i(MAX)))], may_deopt=True)) + assert_rejected(PROVEN, pure_fn([Return(padd(mod(li("n"), i(7)), i(MAX)))], may_deopt=True)) + + +def test_mod_by_an_unknown_divisor_is_full_range(): + ps = [Param("n", I64), Param("m", I64)] + assert_rejected(PROVEN, pure_fn([Return(padd(mod(li("n"), li("m")), i(1)))], params=ps, + may_deopt=True)) + + +def test_floordiv_by_a_positive_constant_halves_the_range(): + two = lambda e: pure_fn([Return(e(div(li("n"), i(2))))], may_deopt=True) + assert_accepted(two(lambda d: padd(d, d))) # [-2**63, 2**63 - 2] + assert_accepted(two(lambda d: pmul(d, i(2)))) + assert_rejected(PROVEN, two(lambda d: pmul(d, i(3)))) + + +def test_floordiv_by_minus_one_is_full_range(): + assert_rejected(PROVEN, pure_fn([Return(padd(div(li("n"), i(-1)), i(1)))], may_deopt=True)) + + +# --- 1d. floor division / modulo by a power of two ------------------------------------------------- + +def _loop(op, start=0, stop=5, params=()): + return fn(params=params, locals_={"k": I64, "t": I64}, pure=True, may_deopt=False, + body=(ForRange("k", i(start), i(stop), i(1), (Assign("t", op(li("k"))),)), Return())) + + +@pytest.mark.parametrize("d", [1, 2, 8, 1024, 2 ** 62]) +def test_proven_floordiv_and_mod_by_a_power_of_two_of_a_non_negative_value_are_accepted(d): + assert_accepted(_loop(lambda k: pdiv(k, i(d)))) + assert_accepted(_loop(lambda k: pmod(k, i(d)))) + + +def test_a_proven_div_is_not_a_deopting_node(): + ok = _loop(lambda k: pdiv(k, i(4))) + assert_accepted(ok) + assert_rejected(FLAG_DEOPT, dataclasses.replace(ok, may_deopt=True)) + + +@pytest.mark.parametrize("op", [pdiv, pmod]) +def test_proven_div_or_mod_needs_a_non_negative_dividend(op): + assert_rejected(PROVEN, _loop(lambda k: op(k, i(4)), start=-1)) + assert_rejected(PROVEN, pure_fn([Return(op(li("n"), i(4)))])) + + +@pytest.mark.parametrize("op", [pdiv, pmod]) +@pytest.mark.parametrize("d", [0, -8, 3, 6, 12]) +def test_proven_div_or_mod_needs_a_positive_power_of_two_constant(op, d): + assert_rejected(PROVEN, _loop(lambda k: op(k, i(d)))) + + +@pytest.mark.parametrize("op", [pdiv, pmod]) +def test_proven_div_or_mod_needs_a_constant_divisor(op): + assert_rejected(PROVEN, _loop(lambda k: op(k, li("k")))) + assert_rejected(PROVEN, _loop(lambda k: op(k, padd(i(2), i(2))))) + + +def test_proven_truediv_is_still_rejected(): + assert_rejected(PROVEN, _loop(lambda k: BinOp(Type.F64, BinOpKind.TRUEDIV, k, i(2), proven=True))) + + +def test_a_guarded_dividend_is_non_negative(): + assert_accepted(pure_fn([If(ge(li("n"), i(0)), (Return(pdiv(li("n"), i(16))),)), Return(i(0))])) + assert_rejected(PROVEN, pure_fn([If(ge(li("n"), i(-1)), (Return(pdiv(li("n"), i(16))),)), + Return(i(0))])) + + +def test_a_mod_result_can_feed_a_proof(): + # (k % 8) + (MAX - 7) fits + assert_accepted(_loop(lambda k: padd(pmod(k, i(8)), i(MAX - 7)))) + assert_rejected(PROVEN, _loop(lambda k: padd(pmod(k, i(8)), i(MAX - 6)))) + + +# --- 2. end to end --------------------------------------------------------------------------------- + +SOURCE = '''\ +# typedpython: compiled + + +def dec_if_positive(n: int) -> int: + if n > 0: + return n - 1 + return -1 + + +def dec_after_guard(n: int) -> int: + if n <= 0: + return 0 + return n - 1 + + +def inc_below_max(n: int) -> int: + if n < 9223372036854775807: + return n + 1 + return 0 + + +def inc_below_100(n: int) -> int: + if n < 100: + return n + 1 + return 0 + + +def inc_if_positive(n: int) -> int: + if n > 0: + return n + 1 + return 0 + + +def reverse_gap(a: int, b: int) -> int: + lo: int = a + hi: int = b + steps: int = 0 + while lo < hi: + lo += 1 + hi -= 1 + steps += 1 + return hi - lo + steps + + +def killed_guard(n: int, a: list[int]) -> int: + if n < 100: + n = a[0] + return n + 1 + return 0 + + +def killed_in_loop(c: int, a: list[int]) -> int: + n: int = a[0] + t: int = 0 + if n < 100: + for i in range(c): + t = n + 1 + n = a[1] + return t + + +def parity(n: int) -> int: + k: int = 0 + s: int = 0 + for i in range(n): + if k % 2 == 0: + s += 1 + k += 1 + return s + + +def const_local() -> int: + x: int = 41 + return x + 1 + + +def range_shifts(a: int, b: int) -> int: + if a < 0: + return 0 + t: int = 0 + for i in range(a, b): + t += i // 4 + i % 8 + i // 1 + return t + + +def guarded_shift(n: int) -> int: + if n >= 0: + return n // 16 + n % 16 + return 0 + + +def negative_shift(n: int) -> int: + return n // 4 + n % 8 + + +def odd_divisor(n: int) -> int: + if n >= 0: + return n // 3 + n % 6 + return 0 + + +def mod_then_add(n: int) -> int: + return n % 10 + 1 + + +def pair_sums(a: list[int]) -> int: + best: int = 0 + for i in range(len(a)): + for j in range(len(a)): + if i + j > best: + best = i + j + return best +''' + +CASES = { + "dec_if_positive": [(n,) for n in (MIN, -1, 0, 1, 2, MAX - 1, MAX)], + "dec_after_guard": [(n,) for n in (MIN, -1, 0, 1, 2, MAX - 1, MAX)], + "inc_below_max": [(n,) for n in (MIN, -1, 0, MAX - 2, MAX - 1, MAX)], + "inc_below_100": [(n,) for n in (MIN, 98, 99, 100, MAX)], + "inc_if_positive": [(n,) for n in (MIN, 0, 1, MAX - 1, MAX)], + "reverse_gap": [(0, 0), (0, 1), (0, 7), (5, 2), (-3, 4), (MAX - 3, MAX), (MIN, MIN + 3), + (MAX - 1, MAX), (MIN, MIN + 1)], + "killed_guard": [(5, [3]), (5, [MAX]), (500, [MAX]), (5, [MIN])], + "killed_in_loop": [(0, [5, MAX]), (1, [5, MAX]), (3, [5, MAX]), (3, [5, 6]), (3, [500, MAX])], + "parity": [(0,), (1,), (2,), (7,), (100,)], + "const_local": [()], + "range_shifts": [(0, 0), (0, 40), (-20, 20), (-3, 0), (3, 3), (5, 2), (MAX - 5, MAX), (MIN, MIN + 6), + (MIN + 3, MIN + 4)], + "guarded_shift": [(n,) for n in (MIN, -1, 0, 1, 15, 16, 17, 1023, MAX)], + "negative_shift": [(n,) for n in (MIN, -9, -8, -1, 0, 1, 7, 8, MAX)], + "odd_divisor": [(n,) for n in (MIN, -1, 0, 1, 5, 6, 7, MAX)], + "mod_then_add": [(n,) for n in (MIN, -11, -1, 0, 9, 10, MAX)], + "pair_sums": [([],), ([5],), ([1, 2, 3],), (list(range(9)),)], +} + + +@pytest.fixture(scope="module") +def built(tmp_path_factory): + d = tmp_path_factory.mktemp("interval_proofs") + path = d / "ivmod.py" + path.write_text(SOURCE) + lowered = frontend.lower(path) + proved, diags = verify.verify(lowered) + c = cgen.generate(proved, path, path.name) + so = cbuild.build(c, path.stem, d / "out") + loader = importlib.machinery.ExtensionFileLoader(path.stem, str(so)) + spec = importlib.util.spec_from_file_location(path.stem, str(so), loader=loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return dataclasses.make_dataclass("Built", ["lowered", "proved", "diags", "c", "module"])( + lowered, proved, diags, c, module) + + +def impl_text(c: str, name: str) -> str: + """The C text of the normal implementation of `name`.""" + start = c.index(f"static int tp_impl_{name}(", c.index("static const char tp_source")) + while not c[c.index(")", start) + 1:].startswith("\n{"): + start = c.index(f"static int tp_impl_{name}(", start + 1) + return c[start:c.index("\n}\n", start)] + + +def helpers(c: str, name: str) -> dict[str, int]: + text = impl_text(c, name) + out: dict[str, int] = {} + for h in re.findall(r"tp_(add|sub|mul|floordiv|mod|neg)_i64\(", text): + out[h] = out.get(h, 0) + 1 + return out + + +def test_every_function_is_compiled(built): + assert {f.name for f in built.proved.functions} == set(CASES), (built.proved.skipped, built.diags) + + +@pytest.mark.parametrize("name", sorted(CASES)) +def test_compiled_equals_cpython_at_the_boundaries(built, name): + comp = getattr(built.module, name) + plain = built.module.__typedpython_interpreted__[name] + for args in CASES[name]: + want = plain(*[list(a) if isinstance(a, list) else a for a in args]) + got = comp(*[list(a) if isinstance(a, list) else a for a in args]) + assert got == want and type(got) is type(want), (name, args, got, want) + + +# which helpers must be gone from each function's C, and which must stay +GONE = { + "dec_if_positive": ["sub"], + "dec_after_guard": ["sub"], + "inc_below_max": ["add"], + "inc_below_100": ["add"], + "const_local": ["add"], + "guarded_shift": ["floordiv", "mod", "add"], + "mod_then_add": ["add"], + "pair_sums": ["add"], +} +STAY = { + "inc_if_positive": ["add"], + "killed_guard": ["add"], + "killed_in_loop": ["add"], + "negative_shift": ["floordiv", "mod"], + "odd_divisor": ["floordiv", "mod"], + "mod_then_add": ["mod"], +} + + +@pytest.mark.parametrize("name", sorted(GONE)) +def test_a_proved_operation_has_no_checked_helper_in_the_c(built, name): + found = helpers(built.c, name) + for h in GONE[name]: + assert h not in found, (name, found) + + +@pytest.mark.parametrize("name", sorted(STAY)) +def test_an_unprovable_operation_keeps_its_checked_helper(built, name): + found = helpers(built.c, name) + for h in STAY[name]: + assert h in found, (name, found) + + +def test_reverse_gap_drops_exactly_the_two_guarded_steps(built): + # lo += 1 and hi -= 1 are proved by `lo < hi`; `steps += 1` and `hi - lo + steps` are not + assert helpers(built.c, "reverse_gap") == {"add": 2, "sub": 1} + + +def test_parity_masks_the_accumulator_and_keeps_its_increments_checked(built): + found = helpers(built.c, "parity") + assert "mod" not in found and found.get("add", 0) >= 2 + assert "& " in impl_text(built.c, "parity") + + +def test_range_shifts_use_shifts_and_masks(built): + text = impl_text(built.c, "range_shifts") + found = helpers(built.c, "range_shifts") + assert "floordiv" not in found and "mod" not in found + assert ">> 2" in text and "& INT64_C(7)" in text + + +def test_the_front_end_marks_the_new_facts_and_the_verifier_agrees(built): + f = {g.name: g for g in built.lowered.functions} + def ops(fn_name): + found, stack = [], [f[fn_name]] + while stack: + x = stack.pop() + if isinstance(x, (tuple, list)): + stack.extend(x) + elif dataclasses.is_dataclass(x) and not isinstance(x, type): + if isinstance(x, BinOp): + found.append(x) + stack.extend(getattr(x, fl.name) for fl in dataclasses.fields(x)) + return found + + assert all(b.proven for b in ops("dec_if_positive")) + assert all(b.proven for b in ops("guarded_shift")) + assert not any(b.proven for b in ops("inc_if_positive")) + assert built.diags == [] diff --git a/python-multiplatform-ksp/src/test/python/test_module_report.py b/python-multiplatform-ksp/src/test/python/test_module_report.py new file mode 100644 index 000000000..8625cf18d --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_module_report.py @@ -0,0 +1,195 @@ +"""Package-relative embedded source path (#112) and the structured per-module report (#128). + +Real Pyrefly, real C builds. SPEC N-8 (embedded path), N-10 (cache), pypackpack's native level. +""" +import re +import shutil +import traceback +import textwrap +from pathlib import Path + +from typedpython import cbuild, cgen, frontend, pipeline, verify + +SRC = "# typedpython: compiled\n" + textwrap.dedent("""\ + def twice(n: int) -> int: + return n * 2 + + + def boom(s: str) -> str: + raise ValueError("boom " + s) + """) + + +def make_project(root: Path) -> Path: + mod = root / "app" / "physics" / "nbody.py" + mod.parent.mkdir(parents=True) + (root / "app" / "__init__.py").write_text("") + (mod.parent / "__init__.py").write_text("") + mod.write_text(SRC) + return mod + + +# --- #112 ---------------------------------------------------------------------------------------- + +def test_generated_c_is_identical_from_two_absolute_roots_and_embeds_the_relative_path( + tmp_path, monkeypatch): + seen = [] + real = cbuild.build + + def build(c_source, *a, **k): + seen.append(c_source) + return real(c_source, *a, **k) + + monkeypatch.setattr(cbuild, "build", build) + roots = [tmp_path / "one" / "rootA", tmp_path / "two" / "deeper" / "rootB"] + for i, root in enumerate(roots): + root.mkdir(parents=True) + mod = make_project(root) + pipeline.compile_project([mod], root, tmp_path / f"cache{i}") + assert len(seen) == 2 and seen[0] == seen[1] + assert 'tp_source_path[] = "app/physics/nbody.py"' in seen[0] + assert str(tmp_path) not in seen[0] # nothing else embeds a host path + + +def test_compile_module_embeds_the_path_relative_to_project_root(tmp_path): + mod = make_project(tmp_path / "r") + r = pipeline.compile_module(mod, tmp_path / "o2", project_root=tmp_path / "r") + ext = cbuild.load("nbody", r.extension) + try: + ext.boom("x") + except ValueError as e: + assert traceback.extract_tb(e.__traceback__)[-1].filename == "app/physics/nbody.py" + else: + raise AssertionError("boom did not raise") + d = pipeline.compile_module(mod, tmp_path / "o3") # default root: the file's directory + try: + cbuild.load("nbody", d.extension).boom("x") + except ValueError as e: + assert traceback.extract_tb(e.__traceback__)[-1].filename == "nbody.py" + + +def test_default_embedded_path_is_the_file_name(tmp_path): + mod = make_project(tmp_path / "r") + lowered, _ = verify.verify(frontend.lower(mod)) + assert 'tp_source_path[] = "nbody.py"' in cgen.generate(lowered, mod) + assert str(tmp_path) not in cgen.generate(lowered, mod) + + +def test_a_moved_file_with_the_same_content_does_not_reuse_the_extension(tmp_path): + root = tmp_path / "p" + (root / "a").mkdir(parents=True) + (root / "b").mkdir() + (root / "a" / "m.py").write_text(SRC) + cache = tmp_path / "cache" + first = pipeline.compile_project([root / "a" / "m.py"], root, cache) + assert first.modules[0].status == "rebuilt" + shutil.move(root / "a" / "m.py", root / "b" / "m.py") + second = pipeline.compile_project([root / "b" / "m.py"], root, cache) + assert second.modules[0].status == "rebuilt" + ext = cbuild.load("m", second.modules[0].extension) + try: + ext.boom("x") + except ValueError as e: + assert traceback.extract_tb(e.__traceback__)[-1].filename == "b/m.py" + + +# --- #128 ---------------------------------------------------------------------------------------- + +MIXED = "# typedpython: compiled\n" + textwrap.dedent("""\ + def ok(n: int) -> int: + return n + 1 + + + def greet(name: str) -> str: + return name[1:] + """) + + +def build_one(tmp_path, rel, source, cache="cache"): + root = tmp_path / "proj" + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source) + r = pipeline.compile_project([path], root, tmp_path / cache) + return root, path, r.modules[0] + + +def test_kind_compiled(tmp_path): + _, _, m = build_one(tmp_path, "pkg/mixed.py", MIXED) + assert m.kind == "compiled" and m.extension is not None + [item] = m.skipped_items + assert item.name == "greet" and item.file == "pkg/mixed.py" + assert item.line == 7 and not item.message.startswith("line ") + assert m.skipped["greet"].startswith("line 7: ") # ModuleResult.skipped unchanged + + +def test_kind_nothing_compiled_with_structured_items(tmp_path): + _, _, m = build_one(tmp_path, "pkg/plain.py", + "# typedpython: compiled\n\ndef f(s: str) -> str:\n return s[1:]\n") + assert m.kind == "nothing_compiled" and m.extension is None + [item] = m.skipped_items + assert (item.name, item.file, item.line) == ("f", "pkg/plain.py", 4) + assert item.message and not re.match(r"line \d+", item.message) + + +def test_kind_not_marked_when_there_is_neither_marker_nor_decorator(tmp_path): + _, _, m = build_one(tmp_path, "pkg/unmarked.py", "def f(x: int) -> int:\n return x\n") + assert m.kind == "not_marked" and m.extension is None and m.skipped_items == [] + + +def test_a_compiled_decorator_alone_opts_in(tmp_path): + src = "@compiled\ndef f(s: str) -> str:\n return s[1:]\n" + _, _, m = build_one(tmp_path, "pkg/deco.py", src) + assert m.kind == "nothing_compiled" + assert [(i.name, i.line) for i in m.skipped_items] == [("f", 3)] + + +def test_class_items_carry_name_and_line(tmp_path): + src = ("# typedpython: compiled\n\n\nclass Node:\n __slots__ = ('a',)\n" + " def __init__(self, a: int) -> None:\n self.a = a\n") + _, _, m = build_one(tmp_path, "pkg/cls.py", src) + cls = [i for i in m.skipped_items if i.name == "Node"] + assert cls and cls[0].line == 4 and cls[0].file == "pkg/cls.py" + + +def test_verifier_skips_carry_a_line_and_no_prefix(tmp_path): + src = "# typedpython: compiled\n\n\ndef f(x: int) -> int:\n return g(x)\n\n\ndef g(x: int) -> int:\n return x\n" + _, _, m = build_one(tmp_path, "pkg/v.py", src) + for i in m.skipped_items: + assert isinstance(i.line, int) and not i.message.startswith("line ") + + +def test_structure_round_trips_through_the_cache_for_reused_modules(tmp_path): + root, path, first = build_one(tmp_path, "pkg/mixed.py", MIXED) + second = pipeline.compile_project([path], root, tmp_path / "cache").modules[0] + assert second.status == "reused" + assert second.kind == first.kind == "compiled" + assert second.skipped_items == first.skipped_items and second.skipped_items + assert second.skipped == first.skipped + + +def test_not_marked_and_nothing_compiled_survive_reuse(tmp_path): + root, path, _ = build_one(tmp_path, "pkg/plain.py", + "# typedpython: compiled\n\ndef f(s: str) -> str:\n return s[1:]\n") + again = pipeline.compile_project([path], root, tmp_path / "cache").modules[0] + assert again.status == "reused" and again.kind == "nothing_compiled" and again.skipped_items + root2, path2, _ = build_one(tmp_path, "pkg/un.py", "def f(x: int) -> int:\n return x\n") + again2 = pipeline.compile_project([path2], root2, tmp_path / "cache").modules[0] + assert again2.status == "reused" and again2.kind == "not_marked" + + +def test_cli_build_prints_file_line_name_message_and_not_marked(tmp_path): + import os + import subprocess + import sys + root = tmp_path / "proj" + (root / "pkg").mkdir(parents=True) + (root / "pkg" / "mixed.py").write_text(MIXED) + (root / "pkg" / "un.py").write_text("def f(x: int) -> int:\n return x\n") + src = Path(__file__).parents[2] / "main" / "python" + out = subprocess.run( + [sys.executable, "-m", "typedpython", "build", str(root), "--cache", str(tmp_path / "c")], + capture_output=True, text=True, env=dict(os.environ, PYTHONPATH=str(src))) + assert out.returncode == 0, out.stdout + out.stderr + assert re.search(r"^\s*pkg/mixed\.py:7: greet: \S", out.stdout, re.M), out.stdout + assert re.search(r"pkg/un\.py.*not marked for compilation", out.stdout), out.stdout diff --git a/python-multiplatform-ksp/src/test/python/test_pipeline.py b/python-multiplatform-ksp/src/test/python/test_pipeline.py index a835153c9..2e43816e4 100644 --- a/python-multiplatform-ksp/src/test/python/test_pipeline.py +++ b/python-multiplatform-ksp/src/test/python/test_pipeline.py @@ -54,11 +54,11 @@ def test_unsupported_functions_stay_interpreted_with_a_reason(write, tmp_path): path = write("mixed.py", SCALAR + """ def greet(name: str) -> str: - return "hi " + name + return name[1:] """) result = pipeline.compile_module(path, tmp_path / "out") assert "greet" in result.skipped and result.skipped["greet"] - assert pipeline.load(result).greet("x") == "hi x" + assert pipeline.load(result).greet("xy") == "y" def test_a_module_with_nothing_to_compile_produces_no_extension(write, tmp_path): @@ -100,3 +100,38 @@ def test_demo_reports_identical_output(write, tmp_path): assert result.returncode == 0, result.stdout + result.stderr assert "identical output" in result.stdout and "249750.000" in result.stdout assert "compiled functions: total" in result.stdout + + +ARRAY_CALLBACK = """\ +# typedpython: compiled +from typing import Callable + + +def first_after(xs: list[float], cb: Callable[[], None]) -> float: + cb() + return xs[0] + + +def fill_then(xs: list[float], cb: Callable[[], None]) -> None: + xs[1] = 5.0 + cb() +""" + + +def test_array_function_with_a_callback_matches_cpython(write, tmp_path): + # #203: the callback changes the list; a copy-in would read or write back stale values + path = write("arrcb.py", ARRAY_CALLBACK) + result = pipeline.compile_module(path, tmp_path / "out") + assert "first_after" in result.skipped and "fill_then" in result.skipped, result.skipped + compiled, ref = pipeline.load(result), interpreted(path) + for mod in (compiled, ref): + xs = [1.0, 2.0, 3.0] + def cb(): + xs[0] = 100.0 + xs[1] = 7.0 + assert mod.first_after(xs, cb) == 100.0 + ys = [1.0, 2.0, 3.0] + def cb2(): + ys[1] = 9.0 + mod.fill_then(ys, cb2) + assert ys == [1.0, 9.0, 3.0] diff --git a/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py b/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py index 9eef09304..dd7cfad86 100644 --- a/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py +++ b/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py @@ -133,7 +133,7 @@ def test_rebuilt_and_reused_extensions_compute_the_interpreted_results(project, def test_module_with_nothing_compiled_has_no_extension_and_is_reused(tmp_path): root = tmp_path / "proj" root.mkdir() - (root / "plain.py").write_text(HEADER + "def f(s: str) -> str:\n return s + 'x'\n") + (root / "plain.py").write_text(HEADER + "def f(s: str) -> str:\n return s[1:]\n") cache = tmp_path / "cache" first = pipeline.compile_project([root / "plain.py"], root, cache) m = first.modules[0] diff --git a/python-multiplatform-ksp/src/test/python/test_runtime.py b/python-multiplatform-ksp/src/test/python/test_runtime.py index c213475a9..629798e75 100644 --- a/python-multiplatform-ksp/src/test/python/test_runtime.py +++ b/python-multiplatform-ksp/src/test/python/test_runtime.py @@ -1203,4 +1203,4 @@ def test_binop_obj_maps_the_six_kinds_to_the_python_operators(rt): assert rt.binop_obj(I64_MAX, 1, 0) == 2**63 assert rt.binop_obj(I64_MIN, -1, 4) == 2**63 assert exc_of(rt.binop_obj, 1, 0, 4)[1:3] == (ZeroDivisionError, str(exc_of(operator.floordiv, 1, 0)[2])) - assert exc_of(rt.binop_obj, 1, 2, 9)[1] is SystemError + assert exc_of(rt.binop_obj, 1, 2, 12)[1] is SystemError # 6..11 are the in-place forms (#150) diff --git a/python-multiplatform-ksp/src/test/python/test_str_dict.py b/python-multiplatform-ksp/src/test/python/test_str_dict.py new file mode 100644 index 000000000..2362033c4 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_str_dict.py @@ -0,0 +1,1005 @@ +"""str and dict code in compiled functions (issue #150, widened; SPEC N-8 / N-9). + +Every operation added here must call the C API the interpreter's bytecode calls, so each test is +differential: the compiled function and `__typedpython_interpreted__[name]` run on two fresh copies +of the arguments and must agree on the result, on the exception (type and repr, so the message and +args), on the final state of every argument and on the log of every user `__hash__` / `__eq__` / +`__index__` / `__format__` / `__repr__` / `__iter__` call (same calls, same order). A test that +expects the compiled path compares `__typedpython_deopts__` before and after, so a function that +silently stayed interpreted fails it. + +Also here: the wordfreq benchmark's `word_of` and `main` compile and print what CPython prints, the +object operations leak no reference, and the exact-type guards of the fast paths are shown to matter +(a mutant header whose guard is "always true" makes a test fail). +""" +from __future__ import annotations + +import contextlib +import dataclasses +import gc +import importlib.util +import io +import shutil +import sys +import textwrap +from pathlib import Path + +import pytest + +from typedpython import cbuild, cgen, frontend, verify + +REPO = Path(__file__).resolve().parents[4] +WORDFREQ = REPO / "benchmarks" / "typedpython" / "py" / "wordfreq.py" + +SOURCE = textwrap.dedent(''' + # typedpython: compiled + from typing import SupportsIndex + + + def get(d: dict[object, object], k: object) -> object: + return d[k] + + + def get_list(xs: list[object], i: int) -> object: + return xs[i] + + + def get_index(xs: list[object], i: SupportsIndex) -> object: + return xs[i] + + + def get_str(s: str, i: int) -> str: + return s[i] + + + def get_nested(d: dict[object, dict[object, object]], a: object, b: object) -> object: + return d[a][b] + + + def put(d: dict[object, object], k: object, v: object) -> None: + d[k] = v + + + def put_list(xs: list[object], i: int, v: object) -> None: + xs[i] = v + + + def has(c: list[object], x: object) -> bool: + return x in c + + + def has_not(c: list[object], x: object) -> bool: + return x not in c + + + def has_dict(c: dict[object, object], x: object) -> bool: + return x in c + + + def has_str(c: str, x: str) -> bool: + return x in c + + + def has_branch(c: list[object], x: object) -> str: + if x in c: + return "yes" + return "no" + + + def has_chain(c: list[object], x: object, y: object) -> bool: + return x in c in y + + + def pairs(xs: list[tuple[object, ...]]) -> list[object]: + out: list[object] = [] + for a, b in xs: + out.append(a) + out.append(b) + return out + + + def triples(xs: list[tuple[object, ...]]) -> list[object]: + out: list[object] = [] + for a, b, c in xs: + out.append(a) + out.append(c) + return out + + + def each(xs: list[object]) -> list[object]: + out: list[object] = [] + for x in xs: + if x is None: + continue + out.append(x) + return out + + + def first_after(xs: list[object], stop: object) -> object: + found: object = None + for x in xs: + if x == stop: + found = x + break + return found + + + def return_inside(xs: list[object]) -> object: + for x in xs: + return x + return None + + + def nested(xs: list[list[object]]) -> list[object]: + out: list[object] = [] + for row in xs: + for x in row: + if x is None: + break + out.append(x) + return out + + + def items_of(d: dict[str, object]) -> list[object]: + out: list[object] = [] + for k, v in d.items(): + out.append(k) + out.append(v) + return out + + + def mutate_while_iterating(d: dict[str, object]) -> object: + n: int = 0 + for k in d: + d["new" + k] = n + n += 1 + return n + + + def fmt(x: object, y: float, n: int) -> str: + return f"{x}|{x!r}|{x!s}|{x!a}|{y:.3f}|{n:05d}|{x:>5}|{n}" + + + def fmt_one(x: object) -> str: + return f"{x}" + + + def fmt_text(x: object) -> str: + return f"a{x}b{x!r}c" + + + def fmt_empty() -> str: + return f"" + + + def fmt_debug(x: object) -> str: + return f"{x=}" + + + def cat(a: str, b: str) -> str: + return a + b + + + def cat_in_place(a: str, b: str) -> str: + s: str = "" + s += a + s += b + return s + + + def grow(xs: list[object], ys: list[object]) -> list[object]: + out: list[object] = [] + out += xs + out += ys + return out + + + def empties() -> object: + d: dict[object, object] = {} + xs: list[object] = [] + xs.append(d) + return xs + + + class Acc: + def __init__(self, log: list[object]) -> None: + self.log = log + + def __iadd__(self, other: int) -> Acc: + self.log.append(("iadd", other)) + return self + + def __isub__(self, other: int) -> Acc: + self.log.append(("isub", other)) + return self + + def __imul__(self, other: int) -> Acc: + self.log.append(("imul", other)) + return self + + def __itruediv__(self, other: int) -> Acc: + self.log.append(("itruediv", other)) + return self + + def __ifloordiv__(self, other: int) -> Acc: + self.log.append(("ifloordiv", other)) + return self + + def __imod__(self, other: int) -> Acc: + self.log.append(("imod", other)) + return self + + + def aug_ops(a: Acc, b: int) -> Acc: + a += b + a -= b + a *= b + a /= b + a //= b + a %= b + return a + + + def aug_list(xs: list[str], n: int) -> list[str]: + xs *= n + xs += xs + return xs + + + def lookup_default(d: dict[str, int], k: str) -> object: + counts: dict[str, int] = {} + counts[k] = counts.get(k, 0) + 1 + counts[k] = counts.get(k, 0) + 1 + return counts[k] + d.get(k, 0) +''') + +COMPILED = { + "get", "get_list", "get_index", "get_str", "get_nested", "put", "put_list", "has", + "has_not", "has_dict", "has_str", "has_branch", "has_chain", "pairs", "triples", "each", + "first_after", "return_inside", "nested", "items_of", "mutate_while_iterating", "fmt", "fmt_one", + "fmt_text", "fmt_empty", "fmt_debug", "cat", "cat_in_place", "grow", "empties", "lookup_default", "aug_ops", "aug_list", +} + + +def build_source(d, source, name="strmod", runtime_dir=None): + path = d / f"{name}.py" + path.write_text(source) + proved, diags = verify.verify(frontend.lower(path)) + c = cgen.generate(proved, path, path.name) + so = cbuild.build(c, path.stem, d / "out", runtime_dir=runtime_dir) + return proved, diags, cbuild.load(so.stem.split(".")[0], so) + + +@dataclasses.dataclass +class Built: + proved: object + diags: list + module: object + + +@pytest.fixture(scope="module") +def built(tmp_path_factory): + d = tmp_path_factory.mktemp("strmod") + proved, diags, m = build_source(d, SOURCE) + return Built(proved, diags, m) + + +def deopts(m): + return m.__typedpython_deopts__ + + +def interp(m, name): + return m.__typedpython_interpreted__[name] + + +def outcome(fn, args): + try: + return ("ok", fn(*args)) + except BaseException as e: # noqa: BLE001 - the type, the message and the args are observable + return ("raise", repr(e)) + + +def state(args): + return repr(args) + + +def differential(m, name, make, expect_deopt=False, logged=False): + """Compiled and interpreted run on two fresh argument lists. With `logged`, `make()` returns + `(args, log)` and the two logs of user-code calls must be equal and not empty.""" + ca, ia = make(), make() + if logged: + (ca, clog), (ia, ilog) = ca, ia + before = deopts(m) + c = outcome(getattr(m, name), ca) + after = deopts(m) + i = outcome(interp(m, name), ia) + assert c == i, (c, i) + assert state(ca) == state(ia), (state(ca), state(ia)) + if logged: + assert clog == ilog and clog, (clog, ilog) + if expect_deopt: + assert after == before + 1, "expected a deopt (the interpreted path), got none" + else: + assert after == before, "unexpected deopt: the compiled path did not run" + return c + + +class K: + """A key whose hash and equality are user code, logging every call. Hash collides on purpose.""" + + def __init__(self, v, log): + self.v, self.log = v, log + + def __hash__(self): + self.log.append(("hash", self.v)) + return hash(self.v) % 3 + + def __eq__(self, other): + self.log.append(("eq", self.v, getattr(other, "v", other))) + return isinstance(other, K) and other.v == self.v + + def __repr__(self): + return f"K({self.v})" + + +class SKey(str): + """A str subclass whose hash and equality are user code.""" + log: list = [] + + def __hash__(self): + SKey.log.append(("hash", str(self))) + return hash(str(self)) + + def __eq__(self, other): + SKey.log.append(("eq", str(self), str(other))) + return str.__eq__(self, other) + + +class EqList: + def __init__(self, v, log): + self.v, self.log = v, log + + def __eq__(self, other): + self.log.append(("eq", self.v, getattr(other, "v", other))) + return getattr(other, "v", other) == self.v + + __hash__ = None + + def __repr__(self): + return f"E({self.v})" + + +class Idx: + def __init__(self, i, log): + self.i, self.log = i, log + + def __index__(self): + self.log.append(("index", self.i)) + return self.i + + def __repr__(self): + return f"Idx({self.i})" + + +class Fmt: + def __init__(self, log): + self.log = log + + def __format__(self, spec): + self.log.append(("format", spec)) + return f"<{spec}>" + + def __repr__(self): + self.log.append(("repr",)) + return "R" + + def __str__(self): + self.log.append(("str",)) + return "S" + + def __eq__(self, other): + return self is other + + __hash__ = object.__hash__ + + +class SubDict(dict): + def __getitem__(self, k): + return ("sub-get", k) + + def __setitem__(self, k, v): + dict.__setitem__(self, k, ("sub-set", v)) + + def __contains__(self, k): + return k == "magic" + + +class MissingDict(dict): + def __missing__(self, k): + return ("missing", k) + + +class SubList(list): + def __getitem__(self, i): + return ("sub-list", i) + + +class SubStr(str): + def __add__(self, other): + return "sub-add" + + def __radd__(self, other): + return "sub-radd" + + +class Three: + def __iter__(self): + return iter([1, 2, 3]) + + def __repr__(self): + return "Three()" + + +class Money: + def __init__(self, v): + self.v = v + + def __add__(self, other): + return ("add", self.v, other) + + def __radd__(self, other): + return ("radd", self.v, other) + + def __repr__(self): + return f"Money({self.v})" + + +def test_compiled_set(built): + names = {f.name for f in built.proved.functions} + assert names >= COMPILED, (sorted(COMPILED - names), + {n: built.proved.skipped.get(n) for n in sorted(COMPILED - names)}) + + +# --- subscript ---------------------------------------------------------------------------------- + +def test_dict_getitem(built): + m = built.module + for key in ("a", "zz", 3, None, (1, 2), 1.0): + differential(m, "get", lambda key=key: [{"a": 1, 3: 4, None: 5, (1, 2): 6, 1: 7}, key]) + differential(m, "get", lambda: [{}, []]) # unhashable key: TypeError + differential(m, "get", lambda: [{}, {"a": 1}]) + differential(m, "get", lambda: [{"a": 1}, SKey("a")]) + + +def test_dict_getitem_user_hash_and_eq_run_the_same_times(built): + m = built.module + for present in ([1], [1, 2, 3, 4], [0, 3, 6, 9]): # colliding hashes: __eq__ runs + for key_value in (1, 3, 5, 9): + def make(present=present, key_value=key_value): + log = [] + d = {K(v, log): v for v in present} + log.clear() + return [d, K(key_value, log)], log + differential(m, "get", make, logged=True) + for same in (True, False): # identity shortcut vs __eq__ + def make(same=same): + log = [] + key = K(1, log) + d = {key: "x", K(4, log): "y"} + log.clear() + return [d, key if same else K(1, log)], log + differential(m, "get", make, logged=True) + + +def test_dict_getitem_subclass_keys_and_dict_subclasses_use_their_own_methods(built): + m = built.module + SKey.log.clear() + differential(m, "get", lambda: [{SKey("a"): 1}, "a"]) + differential(m, "get", lambda: [{"a": 1}, SKey("a")]) + assert SKey.log # the user hash ran + differential(m, "get", lambda: [SubDict({"a": 1}), "a"]) # __getitem__ override + assert m.get(SubDict({"a": 1}), "a") == ("sub-get", "a") + differential(m, "get", lambda: [MissingDict(), "q"]) # __missing__ + assert m.get(MissingDict(), "q") == ("missing", "q") + differential(m, "get_nested", lambda: [{1: {2: 3}}, 1, 2]) + differential(m, "get_nested", lambda: [{1: {2: 3}}, 1, 5]) + differential(m, "get_nested", lambda: [{1: {2: 3}}, 7, 5]) + + +def test_list_and_str_getitem(built): + m = built.module + for i in (0, 1, -1, 2, -3, 3, -4, 100): + differential(m, "get_list", lambda i=i: [[10, 20, 30], i]) + differential(m, "get_str", lambda i=i: ["abc", i]) + differential(m, "get_list", lambda: [SubList([1, 2]), 0]) + differential(m, "get_list", lambda: [(1, 2, 3), 1]) # a tuple is subscriptable too + differential(m, "get_list", lambda: [5, 0]) # TypeError: not subscriptable + differential(m, "get_list", lambda: [None, 0]) + differential(m, "get_list", lambda: [3.5, 0]) + differential(m, "get_list", lambda: [{"a": 1}, 0]) # KeyError(0) + + +def test_getitem_with_user_index(built): + m = built.module + + def make(i, xs=(10, 20, 30)): + def _make(): + log = [] + return [list(xs), Idx(i, log), log] + return _make + for i in (0, 2, -1, 5): + differential(m, "get_index", lambda i=i: make(i)()[:2]) + # the user __index__ call is the same: logged through the argument itself + log_a, log_b = [], [] + a = outcome(m.get_index, [[1, 2, 3], Idx(1, log_a)]) + b = outcome(interp(m, "get_index"), [[1, 2, 3], Idx(1, log_b)]) + assert a == b and log_a == log_b == [("index", 1)] + + +def test_dict_setitem(built): + m = built.module + for key in ("a", "new", 3, None): + differential(m, "put", lambda key=key: [{"a": 1}, key, "v"]) + differential(m, "put", lambda: [{}, [], 1]) # unhashable + differential(m, "put", lambda: [SubDict(), "a", 1]) # __setitem__ override + assert SubDict.__setitem__ is not dict.__setitem__ + d = SubDict() + m.put(d, "a", 1) + assert dict.__getitem__(d, "a") == ("sub-set", 1) + differential(m, "put", lambda: [(1, 2), 0, 1]) # tuple: TypeError + differential(m, "put", lambda: [None, 0, 1]) + differential(m, "put", lambda: [5, 0, 1]) + differential(m, "put_list", lambda: [[1, 2, 3], 1, "x"]) + differential(m, "put_list", lambda: [[1, 2, 3], -3, "x"]) + differential(m, "put_list", lambda: [[1, 2, 3], 3, "x"]) + differential(m, "put_list", lambda: [[1, 2, 3], -4, "x"]) + differential(m, "put_list", lambda: [(1, 2, 3), 0, "x"]) + + +def test_dict_setitem_user_hash_and_eq(built): + m = built.module + for same in (True, False): + def make(same=same): + log = [] + key = K(1, log) + d = {key: "x", K(4, log): "y"} + log.clear() + return [d, key if same else K(1, log), "new"], log + differential(m, "put", make, logged=True) + for v in (7, 4, 10): # new key, colliding key, collision + def make(v=v): + log = [] + d = {K(1, log): 0, K(4, log): 0} + log.clear() + return [d, K(v, log), 1], log + differential(m, "put", make, logged=True) + + +# --- in ---------------------------------------------------------------------------------------- + +def test_contains(built): + m = built.module + for x in (1, 2, 9, None, "a"): + differential(m, "has", lambda x=x: [[1, 2, 3, None, "a"], x]) + differential(m, "has_not", lambda x=x: [[1, 2, 3, None, "a"], x]) + differential(m, "has_dict", lambda x=x: [{1: 1, None: 2, "a": 3}, x]) + differential(m, "has_branch", lambda x=x: [[1, 2, 3], x]) + differential(m, "has_dict", lambda: [{}, []]) # unhashable: TypeError + differential(m, "has", lambda: [5, 1]) # not iterable + differential(m, "has", lambda: [None, 1]) + differential(m, "has_str", lambda: ["hello", "ell"]) + differential(m, "has_str", lambda: ["hello", "xyz"]) + differential(m, "has_str", lambda: ["hello", ""]) + differential(m, "has_dict", lambda: [SubDict(), "magic"]) # __contains__ override + differential(m, "has_dict", lambda: [SubDict(), "other"]) + assert m.has_dict(SubDict(), "magic") is True + differential(m, "has_chain", lambda: [[1, 2], 1, [[1, 2], 5]]) + differential(m, "has_chain", lambda: [[1, 2], 1, [[3], 5]]) + differential(m, "has_chain", lambda: [[1, 2], 9, [[1, 2], 5]]) + + +def test_contains_with_custom_eq_runs_the_same_calls(built): + m = built.module + for needle in (1, 2, 7): + la, lb = [], [] + ca = [EqList(1, la), EqList(2, la), EqList(3, la)] + cb = [EqList(1, lb), EqList(2, lb), EqList(3, lb)] + a = outcome(m.has, [ca, EqList(needle, la)]) + b = outcome(interp(m, "has"), [cb, EqList(needle, lb)]) + assert a == b and la == lb and la, (la, lb) + # identity shortcut: the same object does not call __eq__ (PyObject_RichCompareBool) + la, lb = [], [] + xa, xb = EqList(1, la), EqList(1, lb) + assert outcome(m.has, [[xa], xa]) == outcome(interp(m, "has"), [[xb], xb]) + assert la == lb == [] + nan = float("nan") + assert m.has([nan], nan) is True and interp(m, "has")([nan], nan) is True + + +# --- for --------------------------------------------------------------------------------------- + +def test_for_unpack_and_errors(built): + m = built.module + good = [(1, 2), [3, 4], "ab", {5: 6}.popitem()] + differential(m, "pairs", lambda: [list(good)]) + differential(m, "pairs", lambda: [[]]) + differential(m, "pairs", lambda: [[(1, 2, 3)]]) # too many, exact tuple + differential(m, "pairs", lambda: [[[1, 2, 3]]]) # too many, exact list + differential(m, "pairs", lambda: [["abc"]]) # too many, other type + differential(m, "pairs", lambda: [[{1: 1, 2: 2, 3: 3}]]) # too many, exact dict + differential(m, "pairs", lambda: [[Three()]]) # too many, any iterable + differential(m, "pairs", lambda: [[(1,)]]) # not enough + differential(m, "pairs", lambda: [[()]]) + differential(m, "pairs", lambda: [[""]]) + differential(m, "pairs", lambda: [[5]]) # non-iterable int + differential(m, "pairs", lambda: [[None]]) + differential(m, "pairs", lambda: [[1.5]]) + differential(m, "pairs", lambda: [[(1, 2), (1, 2, 3)]]) # error after one item + differential(m, "triples", lambda: [[(1, 2, 3), (4, 5, 6)]]) + differential(m, "triples", lambda: [[(1, 2)]]) + differential(m, "triples", lambda: [[(1, 2, 3, 4)]]) + differential(m, "triples", lambda: [[{1: 1, 2: 2, 3: 3}]]) + differential(m, "pairs", lambda: [5]) # not iterable: TypeError + differential(m, "pairs", lambda: [None]) + + +def test_for_over_objects_break_continue_return(built): + m = built.module + differential(m, "each", lambda: [[1, None, 2, None, 3]]) + differential(m, "each", lambda: [[]]) + differential(m, "each", lambda: [(1, 2)]) + differential(m, "each", lambda: ["abc"]) + differential(m, "each", lambda: [{1: 1, 2: 2}]) + differential(m, "each", lambda: [range(4)]) + differential(m, "each", lambda: [5]) # TypeError + differential(m, "first_after", lambda: [[1, 2, 3, 4], 3]) + differential(m, "first_after", lambda: [[1, 2, 3, 4], 9]) + differential(m, "return_inside", lambda: [[7, 8]]) + differential(m, "return_inside", lambda: [[]]) + differential(m, "nested", lambda: [[[1, 2, None, 3], [4, None], [5]]]) + differential(m, "nested", lambda: [[[1], 5]]) # inner non-iterable + differential(m, "items_of", lambda: [{"a": 1, "b": 2}]) + differential(m, "items_of", lambda: [{}]) + differential(m, "mutate_while_iterating", lambda: [{"a": 1}]) # RuntimeError: size changed + differential(m, "mutate_while_iterating", lambda: [{}]) + + +def test_for_calls_user_iter_and_next_the_same_times(built): + m = built.module + + class It: + def __init__(self, log, n): + self.log, self.n, self.i = log, n, 0 + + def __iter__(self): + self.log.append("iter") + return self + + def __next__(self): + self.log.append(("next", self.i)) + if self.i >= self.n: + raise StopIteration + self.i += 1 + return self.i + + for n in (0, 3): + la, lb = [], [] + a = outcome(m.each, [It(la, n)]) + b = outcome(interp(m, "each"), [It(lb, n)]) + assert a == b and la == lb and la + + class Boom(It): + def __next__(self): + r = super().__next__() + if r == 2: + raise ValueError("boom") + return r + + la, lb = [], [] + a = outcome(m.each, [Boom(la, 5)]) + b = outcome(interp(m, "each"), [Boom(lb, 5)]) + assert a == b and a[0] == "raise" and la == lb + + +def test_early_exit_releases_the_iterator(built): + m = built.module + died = [] + + class Gen: + def __init__(self, tag): + self.tag = tag + + def __iter__(self): + return iter([1, 2, 3]) + + def __del__(self): + died.append(self.tag) + + class Iter: + def __init__(self, tag): + self.tag = tag + + def __iter__(self): + return self + + def __next__(self): + return 1 + + def __del__(self): + died.append(self.tag) + + for fn, name in ((m.first_after, "c"), (interp(m, "first_after"), "i")): + died.clear() + assert fn(Iter(name), 1) == 1 + gc.collect() + assert died == [name], (name, died) + for fn, name in ((m.return_inside, "c"), (interp(m, "return_inside"), "i")): + died.clear() + fn(Iter(name)) + gc.collect() + assert died == [name] + + +# --- f-strings --------------------------------------------------------------------------------- + +def test_fstring(built): + m = built.module + for x in (1, "s", 2.5, None, True, (1, 2), [1, "a"], b"by", "é", 10**30): + for y, n in ((0.0, 0), (3.14159, 42), (-2.5, -7)): + differential(m, "fmt", lambda x=x, y=y, n=n: [x, y, n]) + differential(m, "fmt_one", lambda: ["str"]) + differential(m, "fmt_one", lambda: [12]) + differential(m, "fmt_one", lambda: [None]) + differential(m, "fmt_text", lambda: [3]) + differential(m, "fmt_empty", lambda: []) + differential(m, "fmt_debug", lambda: [12]) + differential(m, "fmt_debug", lambda: ["q"]) + la, lb = [], [] + a = outcome(m.fmt, [Fmt(la), 1.5, 2]) + b = outcome(interp(m, "fmt"), [Fmt(lb), 1.5, 2]) + assert a == b and la == lb and la, (la, lb) + la, lb = [], [] + outcome(m.fmt_text, [Fmt(la)]) + outcome(interp(m, "fmt_text"), [Fmt(lb)]) + assert la == lb == [("format", ""), ("repr",)] + + +def test_fstring_format_errors(built): + m = built.module + differential(m, "fmt", lambda: [{1: 2}, 1.0, 1]) # bad spec: TypeError + differential(m, "fmt", lambda: [None, 1.0, 1]) + + +# --- str concatenation --------------------------------------------------------------------------- + +def test_str_add(built): + m = built.module + differential(m, "cat", lambda: ["a", "b"]) + differential(m, "cat", lambda: ["", ""]) + differential(m, "cat", lambda: ["é", "ü"]) + differential(m, "cat", lambda: [SubStr("a"), "b"]) # a str subclass: its __add__ + differential(m, "cat", lambda: ["a", SubStr("b")]) # its __radd__ wins (subclass) + assert m.cat(SubStr("a"), "b") == "sub-add" + assert m.cat("a", SubStr("b")) == "sub-radd" + differential(m, "cat", lambda: [1, "b"]) # TypeError text + differential(m, "cat", lambda: ["a", 1]) + differential(m, "cat", lambda: [None, None]) + differential(m, "cat", lambda: [Money(1), "x"]) + differential(m, "cat", lambda: ["x", Money(2)]) + differential(m, "cat", lambda: [b"a", "b"]) + differential(m, "cat", lambda: [1, 2]) # int + int through the object path + differential(m, "cat", lambda: [[1], [2]]) + differential(m, "cat", lambda: [(1,), (2,)]) + differential(m, "cat_in_place", lambda: ["ab", "cd"]) + differential(m, "cat_in_place", lambda: [SubStr("a"), "b"]) + + +def test_augmented_assignment_on_objects_is_in_place(built): + m = built.module + xs, ys = [1], [2] + out = m.grow(xs, ys) + assert out == [1, 2] and xs == [1] and ys == [2] + differential(m, "grow", lambda: [[1], [2]]) + differential(m, "grow", lambda: [(1,), [2]]) # list += tuple extends in place + differential(m, "grow", lambda: [{"a": 1}, "xy"]) # list += dict / str extends too + differential(m, "grow", lambda: [5, [1]]) # TypeError text of the in-place op + differential(m, "grow", lambda: [[1], 5]) + + +def test_every_augmented_operator_on_an_object_is_the_in_place_one(built): + m = built.module + la, lb = [], [] + a, b = m.Acc(la), m.Acc(lb) + before = deopts(m) + assert m.aug_ops(a, 3) is a + assert deopts(m) == before + assert interp(m, "aug_ops")(b, 3) is b + assert la == lb == [("iadd", 3), ("isub", 3), ("imul", 3), ("itruediv", 3), + ("ifloordiv", 3), ("imod", 3)] + xs = ["a", "b"] + assert m.aug_list(xs, 2) is xs and xs == ["a", "b"] * 4 # *= and += extend the same list + differential(m, "aug_list", lambda: [["a", "b"], 3]) + differential(m, "aug_list", lambda: [[], 3]) + differential(m, "aug_list", lambda: [("a", "b"), 2]) # a tuple: *= makes a new one + differential(m, "aug_list", lambda: ["ab", 2]) + + +def test_empty_displays_and_method_calls(built): + m = built.module + differential(m, "empties", lambda: []) + assert m.empties() == [{}] + differential(m, "lookup_default", lambda: [{"a": 5}, "a"]) + differential(m, "lookup_default", lambda: [{"a": 5}, "b"]) + differential(m, "lookup_default", lambda: [{}, "b"]) + + +def test_generated_c_calls_only_documented_helpers(tmp_path): + import re + path = tmp_path / "strmod.py" + path.write_text(SOURCE) + proved, _ = verify.verify(frontend.lower(path)) + c = cgen.generate(proved, path, path.name) + body = re.sub(r"/\*.*?\*/", "", c, flags=re.S) + body = re.sub(r'"(?:\\.|[^"\\])*"', '""', body) + called = {n for n in re.findall(r"\b(tp_[a-z0-9_]+)\s*\(", body)} + api = (cbuild.RUNTIME_DIR / "API.md").read_text() + documented = set(re.findall(r"\b(tp_[a-z0-9_]+)\s*\(", api)) + defined = set(re.findall(r"^static[^\n(]*\b(tp_[a-z0-9_]+)\s*\(", body, flags=re.M)) + for h in ("tp_getitem", "tp_setitem", "tp_contains", "tp_format", "tp_build_string", "tp_unpack", + "tp_get_iter", "tp_iter_next", "tp_new_list", "tp_new_dict"): + assert h in called, f"{h} is not used by the generated C of the test module" + assert sorted(called - documented - defined) == [] + + +# --- no reference leaks -------------------------------------------------------------------------- + +def test_no_reference_is_leaked_or_dropped(built): + m = built.module + key, val, other = object(), object(), object() + obj = type("O", (), {"__eq__": lambda s, o: False, "__hash__": lambda s: 1})() + bag = [obj] + + def churn(): + d = {key: val} + for _ in range(200): + m.get(d, key) + m.put(d, key, val) + m.has([obj], obj) + m.has_not(bag, other) + m.has_dict(d, key) + m.each(bag) + m.pairs([(obj, obj)]) + m.fmt_one(obj) + m.fmt_text(obj) + m.items_of({"a": obj}) + m.grow(bag, bag) + for bad in (lambda: m.get({}, key), lambda: m.pairs([(obj,)]), lambda: m.pairs([(obj,) * 3]), + lambda: m.get_list([obj], 5), lambda: m.put({}, [], obj), + lambda: m.has_dict({}, [obj]), lambda: m.each(5), lambda: m.cat(obj, obj)): + with contextlib.suppress(Exception): + bad() + + churn() + gc.collect() + before = [sys.getrefcount(x) for x in (key, val, other, obj)] + churn() + gc.collect() + after = [sys.getrefcount(x) for x in (key, val, other, obj)] + assert before == after, (before, after) + + +# --- the wordfreq benchmark -------------------------------------------------------------------- + +def run_main(main, n): + old = sys.argv + sys.argv = ["wordfreq", str(n)] + buf = io.StringIO() + try: + with contextlib.redirect_stdout(buf): + main() + finally: + sys.argv = old + return buf.getvalue() + + +@pytest.fixture(scope="module") +def wordfreq(tmp_path_factory): + d = tmp_path_factory.mktemp("wordfreq") + src = "# typedpython: compiled\n" + WORDFREQ.read_text() + proved, diags, m = build_source(d, src, name="wordfreq") + spec = importlib.util.spec_from_file_location("wordfreq_plain", WORDFREQ) + plain = importlib.util.module_from_spec(spec) + spec.loader.exec_module(plain) + return Built(proved, diags, m), plain + + +def test_wordfreq_functions_compile(wordfreq): + built, _ = wordfreq + names = {f.name for f in built.proved.functions} + assert {"next_state", "word_of", "main"} <= names, built.proved.skipped + + +def test_wordfreq_output_equals_cpython(wordfreq): + built, plain = wordfreq + m = built.module + for n in (0, 1, 2, 50, 3000): + compiled = run_main(m.main, n) + assert compiled == run_main(plain.main, n) + assert compiled == run_main(interp(m, "main"), n) + assert deopts(m) == 0 + for i in (0, 1, 25, 26, 17575, 17576, 99999, 10**9): + assert m.word_of(i) == plain.word_of(i) + for bad in (-17576, -20000, True, 1.5, None, "x", 2**70): + assert outcome(m.word_of, [bad]) == outcome(plain.word_of, [bad]) + + +def test_wordfreq_main_n_100000(wordfreq): + built, plain = wordfreq + m = built.module + assert run_main(m.main, 100000) == run_main(interp(m, "main"), 100000) + + +def test_wordfreq_main_errors_equal_cpython(wordfreq): + built, plain = wordfreq + m = built.module + old = sys.argv + try: + for argv in (["w"], ["w", "x"], ["w", "1.5"], ["w", "-3"]): + sys.argv = list(argv) + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + a = outcome(m.main, []) + b = outcome(plain.main, []) + assert a == b, (argv, a, b) + finally: + sys.argv = old + + +# --- the exact-type guards matter --------------------------------------------------------------- + +def mutant_runtime(tmp_path, macro): + """A copy of the runtime header whose fast-path guard `macro` accepts subclasses too.""" + mutant = tmp_path / "runtime" + shutil.copytree(cbuild.RUNTIME_DIR, mutant) + text = (mutant / "tp_runtime.h").read_text() + exact = {"TP_FASTPATH_GUARD_DICT": "PyDict_CheckExact", "TP_FASTPATH_GUARD_STR": "PyUnicode_CheckExact"}[macro] + loose = exact.replace("CheckExact", "Check") + line = f"#define {macro}(o) {exact}(o)" + assert line in text, line + (mutant / "tp_runtime.h").write_text(text.replace(line, f"#define {macro}(o) {loose}(o)")) + return mutant + + +def test_the_fast_path_guards_are_the_single_mutation_points(): + header = (cbuild.RUNTIME_DIR / "tp_runtime.h").read_text() + assert header.count("TP_FASTPATH_GUARD_DICT(") >= 3 # the define and its uses + assert header.count("TP_FASTPATH_GUARD_STR(") >= 2 + + +def test_a_dict_guard_that_accepts_subclasses_is_caught(tmp_path): + """Mutation: the dict fast paths are guarded by the exact type. Built against a header whose + guard is `PyDict_Check`, a subclass overriding __getitem__ / __setitem__ is bypassed, and the + differential tests above fail; here the same module is built both ways to show it.""" + (tmp_path / "good").mkdir() + (tmp_path / "bad").mkdir() + _, _, good = build_source(tmp_path / "good", SOURCE) + _, _, bad = build_source(tmp_path / "bad", SOURCE, + runtime_dir=mutant_runtime(tmp_path, "TP_FASTPATH_GUARD_DICT")) + assert good.get(SubDict({"a": 1}), "a") == ("sub-get", "a") + assert bad.get(SubDict({"a": 1}), "a") == 1 # the mutant skips the override: caught + d, e = SubDict(), SubDict() + good.put(d, "a", 1) + bad.put(e, "a", 1) + assert dict.__getitem__(d, "a") == ("sub-set", 1) + assert dict.__getitem__(e, "a") == 1 + with pytest.raises(AssertionError): + differential(bad, "get", lambda: [SubDict({"a": 1}), "a"]) + + +def test_a_str_guard_that_accepts_subclasses_is_caught(tmp_path): + (tmp_path / "bad").mkdir() + _, _, bad = build_source(tmp_path / "bad", SOURCE, + runtime_dir=mutant_runtime(tmp_path, "TP_FASTPATH_GUARD_STR")) + assert bad.cat(SubStr("a"), "b") == "ab" # the override ("sub-add") is skipped + with pytest.raises(AssertionError): + differential(bad, "cat", lambda: [SubStr("a"), "b"]) diff --git a/python-multiplatform-ksp/src/test/python/test_verify.py b/python-multiplatform-ksp/src/test/python/test_verify.py index 38063296c..6a2d8155d 100644 --- a/python-multiplatform-ksp/src/test/python/test_verify.py +++ b/python-multiplatform-ksp/src/test/python/test_verify.py @@ -179,7 +179,7 @@ def test_read_in_loop_body_before_its_assignment_in_the_body_is_rejected(): def test_continue_skips_the_rest_of_the_loop_body(): # `continue` before the assignment: the read after the If in the body is reached only on the - # path that assigned x — accepted. The read after the loop is not (zero iterations). + # path that assigned x, accepted. The read after the loop is not (zero iterations). body_ok = (If(lb("c"), (Continue(),), (Assign("x", i(1)),)), ExprStmt(li("x"))) assert_accepted(fn(params=[Param("c", BOOL)], locals_={"x": I64}, body=[While(lb("c"), body_ok), Return()])) @@ -646,7 +646,7 @@ def test_scalar_into_an_obj_slot_without_box_is_rejected(): def test_obj_from_a_node_that_does_not_create_a_reference_is_rejected(): - assert_rejected(OWNERSHIP, fn(returns=OBJ, body=[Return(Const(OBJ, None))])) + # Const(OBJ, None) creates a reference since N-11 (`return None`); see test_verify_classes.py. assert_rejected(OWNERSHIP, fn(params=[Param("o", OBJ)], returns=OBJ, body=[Return(UnaryOp(OBJ, UnaryOpKind.NEG, lo("o")))])) assert_rejected(OWNERSHIP, fn(params=[ArrayParam("a", F64A, stored=False)], returns=OBJ, @@ -743,10 +743,11 @@ def test_duplicate_function_names_are_rejected(): # The verifier never trusts `proven`: it computes an interval for every I64 expression and accepts # a proven op only when the exact result interval fits in i64. Intervals (verify.py documents them): # Const [v, v]; a ForRange var inside its own loop [lo(start), hi(stop)-1] (step > 0) or -# [lo(stop)+1, hi(start)] (step < 0); Len [0, 2**60-1]; ADD/SUB/MUL/NEG/POS by interval arithmetic; +# [lo(stop)+1, hi(start)] (step < 0); Len [0, 2**62]; ADD/SUB/MUL/NEG/POS by interval arithmetic; # MOD by a divisor whose interval is positive [0, hi(divisor)-1]; FLOORDIV by a positive divisor -# interval by its endpoints; everything else (params, entry globals, other locals, Index, Unbox, -# Call) the full i64 range. +# interval by its endpoints; a local assigned only i64 values (not a loop var) the join of their +# intervals, narrowed by the guards around the read (#148, test_interval_proofs.py); everything else +# (params, entry globals, Index, Unbox, Call) the full i64 range. def _ranged(op, start=0, stop=5, step=1, impure=True, pure=False, may_deopt=False, params=()): """for k in range(start, stop, step): t = in an (impure) function""" @@ -843,27 +844,35 @@ def test_range_up_to_a_param_bounds_the_var_by_the_param_range(): def test_len_bound_is_zero_to_two_to_the_62(): - a = [ArrayParam("a", F64A, stored=False)] + # impure through a store into the array itself (an array function may not call user code, #203) + a = [ArrayParam("a", F64A, stored=True)] L = Len(I64, "a") - assert_accepted(fn(params=a, returns=I64, body=[IMPURE, Return(padd(L, i(2 ** 62 - 1)))])) + W = If(Compare(BOOL, CompareKind.GT, L, i(0)), (StoreIndex("a", i(0), f(0.0)),)) + assert_accepted(fn(params=a, returns=I64, body=[W, Return(padd(L, i(2 ** 62 - 1)))])) assert_rejected(PROVEN, fn(params=a, returns=I64, - body=[IMPURE, Return(padd(L, i(2 ** 62)))])) - assert_rejected(PROVEN, fn(params=a, returns=I64, body=[IMPURE, Return(pmul(L, i(2)))])) - assert_accepted(fn(params=a, returns=I64, body=[IMPURE, Return(pmul(L, i(-2)))])) # -2**63 - assert_rejected(PROVEN, fn(params=a, returns=I64, body=[IMPURE, Return(pmul(L, i(-3)))])) + body=[W, Return(padd(L, i(2 ** 62)))])) + assert_rejected(PROVEN, fn(params=a, returns=I64, body=[W, Return(pmul(L, i(2)))])) + assert_accepted(fn(params=a, returns=I64, body=[W, Return(pmul(L, i(-2)))])) # -2**63 + assert_rejected(PROVEN, fn(params=a, returns=I64, body=[W, Return(pmul(L, i(-3)))])) -def test_mod_and_floordiv_are_full_range_so_nothing_is_provable_from_them(): +def test_mod_and_floordiv_by_a_constant_have_bounded_results(): + # #148: n % 7 is in [0, 6] (it was full range before) ps = [Param("n", I64)] m = BinOp(I64, BinOpKind.MOD, li("n"), i(7)) - assert_rejected(PROVEN, fn(params=ps, returns=I64, pure=True, may_deopt=True, - body=[Return(padd(m, i(1)))])) + assert_accepted(fn(params=ps, returns=I64, pure=True, may_deopt=True, + body=[Return(padd(m, i(1)))])) + # ... and by an unknown divisor it is still full range + ps2 = [Param("n", I64), Param("d", I64)] + full = BinOp(I64, BinOpKind.MOD, li("n"), li("d")) + assert_rejected(PROVEN, fn(params=ps2, returns=I64, pure=True, may_deopt=True, + body=[Return(padd(full, i(1)))])) def test_what_the_intervals_cannot_prove(): - # a local assigned a constant is still full range (locals other than loop vars are not tracked) - assert_rejected(PROVEN, fn(locals_={"x": I64}, returns=I64, - body=[IMPURE, Assign("x", i(1)), Return(padd(li("x"), i(1)))])) + # a local assigned an unknown value is full range (#148: a local assigned a constant is not) + assert_rejected(PROVEN, fn(params=[Param("n", I64)], locals_={"x": I64}, returns=I64, + body=[IMPURE, Assign("x", li("n")), Return(padd(li("x"), i(1)))])) # the loop var after its loop is full range assert_rejected(PROVEN, fn(locals_={"k": I64}, returns=I64, body=[ IMPURE, Assign("k", i(0)), ForRange("k", i(0), i(5), i(1), ()), Return(padd(li("k"), i(1)))])) @@ -881,7 +890,7 @@ def test_proven_flag_survives_verification(): # --- 8. Call.redo --------------------------------------------------------------------------------- def _callee(name="g", returns=F64, pure=True, may_deopt=True): - """g(n: int) — deopts on n * 2 overflow when may_deopt; returns `returns`.""" + """g(n: int), deopts on n * 2 overflow when may_deopt; returns `returns`.""" prod = BinOp(I64, BinOpKind.MUL, li("n"), i(2)) if may_deopt else li("n") value = {F64: ToFloat(F64, prod), I64: prod, BOOL: lt(prod, i(0)), NONE: None}[returns] body = ([ExprStmt(prod), Return()] if returns is NONE else [Return(value)]) @@ -978,10 +987,10 @@ def test_entry_global_in_a_function_running_user_code_is_rejected(node): def test_the_same_open_node_without_entry_globals_is_accepted(): - # The open node alone is fine: only entry globals need the function closed. - a = ArrayParam("a", F64A, stored=False) - assert_accepted(fn(params=[a], returns=F64, body=[ - ExprStmt(CallObject(OBJ, Global(OBJ, "print"), (), ())), Return(Index(F64, "a", i(0)))])) + # The open node alone is fine: only entry globals (and array parameters, #203) need the + # function closed. + assert_accepted(fn(params=[Param("x", F64)], returns=F64, body=[ + ExprStmt(CallObject(OBJ, Global(OBJ, "print"), (), ())), Return(lf("x"))])) def test_global_read_and_box_keep_a_function_closed(): @@ -1074,9 +1083,10 @@ def _base_functions(): Return(add(lf("s"), MathCall(F64, MathFunc.SQRT, (ToFloat(F64, li("c")),)), F64)), ]) - impure = fn("scale", params=[ArrayParam("a", F64A, stored=True), Param("x", F64), - Param("o", OBJ), Param("c", BOOL)], - locals_={"k": I64, "r": OBJ, "y": F64}, returns=F64, line=20, body=[ + # a local array, not an ArrayParam: this function calls user code (#203) + impure = fn("scale", params=[Param("x", F64), Param("o", OBJ), Param("c", BOOL)], + locals_={"a": F64A, "k": I64, "r": OBJ, "y": F64}, returns=F64, line=20, body=[ + Assign("a", na(F64A, i(4), f(1.0))), Assign("r", call_obj(GetAttr(OBJ, g_("Modifier"), "padding"), Box(OBJ, lf("x")), lo("o"), kw=("dp",))), Assign("y", Unbox(F64, lo("r"))), @@ -1147,6 +1157,10 @@ def _breaking_mutations(rng, g): if kind == 4: # flip may_deopt return "flip may_deopt", dataclasses.replace(g, may_deopt=not g.may_deopt), { FLAG_DEOPT, DEOPT} + if kind == 5 and not any(isinstance(p, ArrayParam) for p in g.params): + kind = 3 # no ArrayParam (scale has a local array since #203): flip pure instead + if kind == 3: + return "flip pure", dataclasses.replace(g, pure=not g.pure), {FLAG_PURE, DEOPT, FLAG_DEOPT} if kind == 5: # flip stored on the first array k = next(j for j, p in enumerate(g.params) if isinstance(p, ArrayParam)) p = g.params[k] @@ -1190,7 +1204,7 @@ def _breaking_mutations(rng, g): return "drop box", _replace_body(g, bpath, bx.operand), {OWNERSHIP, TYPE} return "drop box", _replace_body(g, path, Box(OBJ, e)) if e.type in (I64, F64, BOOL) \ else _replace_body(g, path, Unbox(F64, e)), {OWNERSHIP, TYPE, STRUCTURE} - # kind 13: drop the first Assign (its local becomes possibly unassigned) — breaking because the + # kind 13: drop the first Assign (its local becomes possibly unassigned), breaking because the # local is read later, which holds for the first Assign of both base functions. k = next(j for j, s in enumerate(g.body) if isinstance(s, Assign)) return "drop assign", dataclasses.replace(g, body=g.body[:k] + g.body[k + 1:]), {DA} @@ -1297,7 +1311,7 @@ def _proof_mutations(rng, base): return "proven on param add", b, "q", {PROVEN} if kind == 2: # proven set on a node that has no i64 overflow node = rng.choice([BinOp(F64, BinOpKind.ADD, f(1.0), f(2.0), proven=True), - BinOp(I64, BinOpKind.FLOORDIV, i(4), i(2), proven=True), + BinOp(I64, BinOpKind.FLOORDIV, i(-4), i(2), proven=True), # negative dividend UnaryOp(I64, UnaryOpKind.POS, i(1), proven=True), BinOp(I64, BinOpKind.ADD, i(1), i(2), proven=1)]) t = Type.F64 if node.type is F64 else Type.I64 @@ -1915,7 +1929,7 @@ def _array_functions(): StoreIndex("a", li("k"), BinOp(F64, BinOpKind.ADD, Index(F64, "z", li("k")), f(1.0))), )), - Assign("r", call_obj(g_("print"), Box(OBJ, Index(F64, "z", i(0))))), + Assign("r", Box(OBJ, Index(F64, "z", i(0)))), # no user call: an ArrayParam (#203) Return(), ]) sink = fn("sink", params=[Param("x", OBJ)], returns=NONE, line=12, body=[Return()]) @@ -2021,3 +2035,40 @@ def test_each_array_mutation_kind_was_exercised(): base = _array_functions() kinds = {_array_mutations(rng, base)[0] for _ in range(400)} assert len(kinds) == 18, sorted(kinds) # 16 kinds; kinds 7 and 8 have two variants + + +# --- #203: an ArrayParam function must be closed --------------------------------------------- + +ARRAY_OPEN = "verify/array-open" + + +def _arr_fn(name, body, returns=F64, locals_=None, params_extra=()): + return fn(name, params=[ArrayParam("a", F64A, stored=False), *params_extra], returns=returns, + locals_=locals_, body=body) + + +def test_array_function_calling_an_object_is_rejected(): + # the callback can change the list the function copied in (#203) + assert_rejected(ARRAY_OPEN, _arr_fn("f", [ExprStmt(pycall("cb")), Return(Index(F64, "a", i(0)))])) + + +def test_array_function_reading_an_attribute_is_rejected(): + body = [ExprStmt(GetAttr(OBJ, lo("o"), "x")), Return(Index(F64, "a", i(0)))] + assert_rejected(ARRAY_OPEN, _arr_fn("f", body, params_extra=[Param("o", OBJ)])) + + +def test_array_function_testing_an_object_is_rejected(): + body = [If(Truth(BOOL, lo("o")), (Return(f(0.0)),)), Return(Index(F64, "a", i(0)))] + assert_rejected(ARRAY_OPEN, _arr_fn("f", body, params_extra=[Param("o", OBJ)])) + + +def test_array_function_calling_an_open_function_is_rejected(): + g = fn("g", returns=NONE, body=[ExprStmt(pycall("cb")), Return()]) + caller = _arr_fn("f", [ExprStmt(Call(NONE, "g", ())), Return(Index(F64, "a", i(0)))]) + assert_rejected(ARRAY_OPEN, caller, g) + + +def test_closed_array_function_is_still_accepted(): + g = fn("g", params=[Param("x", F64)], returns=F64, body=[Return(lf("x"))]) + caller = _arr_fn("f", [Return(Call(F64, "g", (Index(F64, "a", i(0)),)))]) + assert_accepted(caller, g) diff --git a/python-multiplatform-ksp/src/test/python/test_verify_classes.py b/python-multiplatform-ksp/src/test/python/test_verify_classes.py new file mode 100644 index 000000000..e08a04134 --- /dev/null +++ b/python-multiplatform-ksp/src/test/python/test_verify_classes.py @@ -0,0 +1,732 @@ +"""The IR safety verifier for fixed-layout classes (SPEC N-9 safety + N-11; IR contract 158a9b4b). + +Every test builds IR by hand: the verifier must prove, never trust the front end, that + + * every class named by FieldGet/FieldSet/New/IsExact/CheckExact/Param.cls is a ClassDecl of the + module, every field named belongs to it, and New is used only for trivial_init classes with one + OBJ argument per field; + * a FieldGet/FieldSet's object is proved exactly the class (`verify/field-unproved` otherwise); + * CheckExact (can deopt) is only in pure functions, FieldSet is an effect, FieldGet/Is/IsExact/New + are not; `pure` and `may_deopt` are recomputed and compared; + * the node types are right and the OBJ-creating nodes follow the ownership rule. + +The last section mutates correct class-using functions at random (seeded) and checks that every +property-breaking mutation is caught. +""" +from __future__ import annotations + +import dataclasses +import random + +import pytest + +from typedpython import ir +from typedpython.ir import ( + And, ArrayParam, Assign, Break, CheckExact, ClassDecl, Const, ExprStmt, FieldGet, FieldSet, + ForRange, Function, GetAttr, Global, If, Is, IsExact, Len, Local, Module, New, Or, Param, + Return, StoreIndex, Type, UnaryOp, UnaryOpKind, While, CallObject, Box, Call, BinOp, BinOpKind, +) + +I64, F64, BOOL, NONE, OBJ = Type.I64, Type.F64, Type.BOOL, Type.NONE, Type.OBJ +F64A = Type.F64_ARRAY + +TYPE = "verify/type" +OWNERSHIP = "verify/ownership" +DEOPT = "verify/deopt-impure" +FLAG_DEOPT = "verify/flag-may-deopt" +FLAG_PURE = "verify/flag-pure" +STRUCTURE = "verify/structure" +CLASS = "verify/class" +FIELD = "verify/field-unproved" +ENTRY_OPEN = "verify/entry-global-open" +UNVERIFIED = "verify/call-unverified" + +POINT = ClassDecl("Point", ("x", "y"), True) +OTHER = ClassDecl("Other", ("x", "z"), True) +SLOWINIT = ClassDecl("SlowInit", ("a",), False) +CLASSES = (POINT, OTHER, SLOWINIT) + + +def _verify(): + # Imported lazily so that, before the class rules exist, each test fails on its own assertion. + from typedpython import verify + return verify + + +# --- builders ------------------------------------------------------------------------------------ + +def lo(n): return Local(OBJ, n) +def li(n): return Local(I64, n) +def g_(n): return Global(OBJ, n) +def fget(obj, field, cls="Point"): return FieldGet(OBJ, obj, cls, field) +def fset(obj, field, value, cls="Point"): return FieldSet(obj, cls, field, value) +def exact(obj, cls="Point"): return IsExact(BOOL, obj, cls) +def check(obj, cls="Point"): return CheckExact(OBJ, obj, cls) +def new(*args, cls="Point"): return New(OBJ, cls, tuple(args)) +def none(): return Const(OBJ, None) +def is_none(x, negate=False): return Is(BOOL, x, none(), negate) +def not_(x): return UnaryOp(BOOL, UnaryOpKind.NOT, x) +def pt(name="p", optional=False, cls="Point"): return Param(name, OBJ, cls, optional) +def obj(name="q"): return Param(name, OBJ) +def band(a, b): return And(BOOL, a, b) +def bor(a, b): return Or(BOOL, a, b) +IMPURE = ExprStmt(CallObject(OBJ, Global(OBJ, "print"), (), ())) + + +def fn(name="f", body=(), params=(), locals_=None, returns=OBJ, pure=True, may_deopt=False, + line=10, entry_globals=()): + return Function(name=name, params=tuple(params), returns=returns, locals=dict(locals_ or {}), + body=tuple(body), pure=pure, may_deopt=may_deopt, source_line=line, + entry_globals=tuple(entry_globals)) + + +def run(*functions, classes=CLASSES): + return _verify().verify(Module("m", tuple(functions), {}, tuple(classes))) + + +def kept(out): + return [g.name for g in out.functions] + + +def rules_for(diags, name): + return {d.rule for d in diags if d.function == name} + + +def accepted(*functions, classes=CLASSES): + out, diags = run(*functions, classes=classes) + assert kept(out) == [g.name for g in functions], [dataclasses.astuple(d) for d in diags] + assert diags == [] + return out + + +def rejected(rule, *functions, name="f", classes=CLASSES): + out, diags = run(*functions, classes=classes) + assert name not in kept(out) + assert name in out.skipped + assert rule in rules_for(diags, name), [dataclasses.astuple(d) for d in diags] + return out, diags + + +def getter(body, params=(pt(),), locals_=None, **kw): + """A pure function returning an OBJ read from fields (the common shape of these tests).""" + return fn(body=body, params=params, locals_=locals_, **kw) + + +# --- the module keeps its classes ---------------------------------------------------------------- + +def test_the_verified_module_keeps_its_valid_classes(): + out = accepted(getter([Return(fget(lo("p"), "x"))])) + assert out.classes == CLASSES + + +# --- 1. class and field names -------------------------------------------------------------------- + +def test_a_function_without_classes_is_unaffected(): + accepted(fn(params=[obj()], body=[Return(lo("q"))]), classes=()) + + +@pytest.mark.parametrize("stmt", [ + lambda: Return(fget(lo("p"), "x", cls="Nowhere")), + lambda: Return(new(g_("a"), g_("b"), cls="Nowhere")), + lambda: If(exact(lo("p"), "Nowhere"), (Return(g_("a")),)), + lambda: Return(check(lo("p"), "Nowhere")), + lambda: fset(lo("p"), "x", g_("v"), cls="Nowhere"), +]) +def test_every_node_must_name_a_class_of_the_module(stmt): + rejected(CLASS, fn(params=[pt()], pure=False, body=[stmt(), Return(g_("r"))])) + + +def test_a_param_may_only_name_a_class_of_the_module(): + rejected(CLASS, fn(params=[pt(cls="Nowhere")], body=[Return(lo("p"))])) + rejected(CLASS, fn(params=[pt(cls="Nowhere", optional=True)], body=[Return(lo("p"))])) + + +def test_a_field_must_belong_to_the_named_class(): + rejected(CLASS, getter([Return(fget(lo("p"), "z"))])) # z is Other's field + rejected(CLASS, fn(params=[pt()], pure=False, body=[fset(lo("p"), "nope", g_("v")), Return(lo("p"))])) + accepted(getter([Return(fget(lo("p"), "y"))])) + + +def test_new_needs_a_trivial_init_class_and_one_object_per_field(): + accepted(getter([Return(new(g_("a"), g_("b")))], params=())) + rejected(CLASS, getter([Return(new(g_("a")))], params=())) + rejected(CLASS, getter([Return(new(g_("a"), g_("b"), g_("c")))], params=())) + rejected(CLASS, getter([Return(new(cls="Point"))], params=())) + rejected(CLASS, getter([Return(new(g_("a"), cls="SlowInit"))], params=())) # not trivial_init + accepted(getter([Return(new(g_("a"), g_("b"), cls="Other"))], params=())) + + +def test_new_arguments_must_be_objects(): + rejected(OWNERSHIP, getter([Return(new(Const(I64, 1), g_("b")))], params=())) + accepted(getter([Return(new(Box(OBJ, Const(I64, 1)), g_("b")))], params=())) + + +def test_param_cls_only_on_obj_params_and_optional_only_with_cls(): + rejected(TYPE, fn(params=[Param("n", I64, "Point")], body=[Return(g_("r"))])) + rejected(TYPE, fn(params=[Param("n", OBJ, None, True)], body=[Return(lo("n"))])) + rejected(TYPE, fn(params=[Param("n", OBJ, "Point", 1)], body=[Return(lo("n"))])) + rejected(TYPE, fn(params=[], entry_globals=[Param("N", I64, "Point")], returns=NONE, + body=[Return()])) + + +@pytest.mark.parametrize("bad, why", [ + (ClassDecl("Dup", ("a",), True), "dup"), + (ClassDecl("Twice", ("a", "a"), True), "field twice"), + (ClassDecl("not an id", ("a",), True), "name"), + (ClassDecl("Fld", ("a b",), True), "field name"), + (ClassDecl("Ti", ("a",), 1), "trivial_init"), +]) +def test_a_malformed_class_decl_is_reported_and_unusable(bad, why): + classes = (bad, bad) if why == "dup" else (bad,) + f = getter([Return(fget(lo("p"), "a", cls=bad.name))], params=(pt(cls=bad.name),)) + out, diags = run(f, classes=classes) + assert any(d.rule == CLASS and d.function == bad.name for d in diags), diags + assert bad.name not in [c.name for c in out.classes] + assert "f" not in kept(out) # the function that used it is not compiled + assert CLASS in rules_for(diags, "f") + + +def test_a_class_with_the_name_of_a_function_is_rejected(): + out, diags = run(getter([Return(lo("p"))], params=(pt(),), name="Point")) + assert any(d.rule == CLASS and d.function == "Point" for d in diags) + assert out.classes == (OTHER, SLOWINIT) + + +# --- 2. the exact-type proof --------------------------------------------------------------------- + +def test_exact_param_is_proved(): + accepted(getter([Return(fget(lo("p"), "x"))])) + + +def test_a_param_exact_proof_is_for_its_own_class(): + rejected(FIELD, getter([Return(fget(lo("p"), "x", cls="Other"))])) + + +def test_a_plain_object_param_is_not_proved(): + rejected(FIELD, getter([Return(fget(lo("q"), "x"))], params=(obj(),))) + + +def test_an_optional_param_alone_is_not_proved(): + rejected(FIELD, getter([Return(fget(lo("p"), "x"))], params=(pt(optional=True),))) + + +def test_check_exact_is_the_proof_for_its_own_class_only(): + accepted(getter([Return(fget(check(lo("q")), "x"))], params=(obj(),), may_deopt=True)) + rejected(FIELD, getter([Return(fget(check(lo("q"), "Other"), "x"))], params=(obj(),), + may_deopt=True)) + + +def test_new_is_the_proof_for_its_own_class_only(): + accepted(getter([Return(fget(new(g_("a"), g_("b")), "x"))], params=())) + rejected(FIELD, getter([Return(fget(new(g_("a"), g_("b"), cls="Other"), "x"))], params=())) + + +def test_a_local_assigned_from_a_new_or_check_exact_is_proved_until_reassigned(): + accepted(getter([Assign("t", new(g_("a"), g_("b"))), Return(fget(lo("t"), "x"))], + params=(), locals_={"t": OBJ})) + accepted(getter([Assign("t", check(lo("q"))), Return(fget(lo("t"), "x"))], params=(obj(),), + locals_={"t": OBJ}, may_deopt=True)) + rejected(FIELD, getter([Assign("t", new(g_("a"), g_("b"))), Assign("t", g_("other")), + Return(fget(lo("t"), "x"))], params=(), locals_={"t": OBJ})) + # a copy of a proved local is proved; a copy of an unproved one is not + accepted(getter([Assign("t", lo("p")), Return(fget(lo("t"), "x"))], locals_={"t": OBJ})) + rejected(FIELD, getter([Assign("t", lo("q")), Return(fget(lo("t"), "x"))], params=(obj(),), + locals_={"t": OBJ})) + + +def test_a_local_assigned_only_new_of_one_class_is_proved_everywhere(): + two = [If(exact(lo("q")), (Assign("t", new(g_("a"), g_("b"))),), + (Assign("t", new(g_("c"), g_("d"))),)), Return(fget(lo("t"), "x"))] + accepted(getter(two, params=(obj(),), locals_={"t": OBJ})) + # in a loop, flow-insensitively: the read comes before the second assignment in the text + loop = [Assign("t", new(g_("a"), g_("b"))), Assign("u", g_("o")), + While(exact(g_("zz")), (Assign("u", fget(lo("t"), "x")), Assign("t", new(g_("c"), g_("d"))), + Break())), Return(lo("u"))] + accepted(getter(loop, params=(), locals_={"t": OBJ, "u": OBJ}, pure=False)) + + +def test_one_other_assignment_to_the_local_ends_the_new_only_form(): + other = [Assign("t", new(g_("a"), g_("b"))), If(exact(g_("zz")), (Assign("t", g_("o")),)), + Return(fget(lo("t"), "x"))] + rejected(FIELD, getter(other, params=(), locals_={"t": OBJ}, pure=False)) + mixed = [Assign("t", new(g_("a"), g_("b"))), If(exact(g_("zz")), + (Assign("t", new(g_("a"), g_("b"), cls="Other")),)), + Return(fget(lo("t"), "x"))] + rejected(FIELD, getter(mixed, params=(), locals_={"t": OBJ}, pure=False)) + # a parameter is never new-only, even if every body assignment is a New + rejected(FIELD, getter([If(exact(g_("zz")), (Assign("q", new(g_("a"), g_("b"))),)), + Return(fget(lo("q"), "x"))], params=(obj(),), pure=False)) + # the flow-sensitive forms still apply to a local with other assignments: the last assignment + # before the read is a New, so form (d) proves it (the front end need not rely on this) + accepted(getter([Assign("t", g_("o")), Assign("t", new(g_("a"), g_("b"))), + Return(fget(lo("t"), "x"))], params=(), locals_={"t": OBJ})) + + +def test_a_reassigned_param_loses_its_proof(): + rejected(FIELD, getter([Assign("p", g_("other")), Return(fget(lo("p"), "x"))])) + accepted(getter([Assign("p", new(g_("a"), g_("b"))), Return(fget(lo("p"), "x"))])) + + +def test_a_dominating_is_exact_is_the_proof(): + body = [If(exact(lo("q")), (Return(fget(lo("q"), "x")),)), Return(g_("r"))] + accepted(getter(body, params=(obj(),))) + + +def test_is_exact_proves_only_its_branch_local_and_class(): + p = (obj(),) + rejected(FIELD, getter([If(exact(lo("q")), (), (Return(fget(lo("q"), "x")),)), + Return(g_("r"))], params=p)) # else branch + rejected(FIELD, getter([If(exact(lo("q")), ()), Return(fget(lo("q"), "x"))], params=p)) + rejected(FIELD, getter([If(exact(lo("q"), "Other"), (Return(fget(lo("q"), "x")),)), + Return(g_("r"))], params=p)) # wrong class + rejected(FIELD, getter([If(exact(lo("q")), (Return(fget(lo("w"), "x")),)), Return(g_("r"))], + params=(obj(), obj("w")))) # another local + rejected(FIELD, getter([If(exact(g_("k")), (Return(fget(g_("k"), "x")),)), Return(g_("r"))], + params=p)) # not a local at all + + +def test_an_assignment_between_the_guard_and_the_access_kills_the_proof(): + body = [If(exact(lo("q")), (Assign("q", g_("o")), Return(fget(lo("q"), "x")))), Return(g_("r"))] + rejected(FIELD, getter(body, params=(obj(),))) + body = [If(exact(lo("q")), (Assign("q", new(g_("a"), g_("b"))), Return(fget(lo("q"), "x")))), + Return(g_("r"))] + accepted(getter(body, params=(obj(),))) # ... unless it is itself proved + + +def test_the_condition_forms_that_prove(): + p = (obj(),) + # And: the left test holds in the right operand and in the then branch + accepted(getter([If(band(exact(lo("q")), Is(BOOL, fget(lo("q"), "x"), g_("k"))), + (Return(fget(lo("q"), "y")),)), Return(g_("r"))], params=p)) + # `not exact` false in the else + accepted(getter([If(not_(exact(lo("q"))), (), (Return(fget(lo("q"), "x")),)), + Return(g_("r"))], params=p)) + # Or: the right operand runs when the left was false + accepted(getter([If(bor(not_(exact(lo("q"))), Is(BOOL, fget(lo("q"), "x"), g_("k"))), + (Return(g_("r")),)), Return(g_("r"))], params=p)) + # ... but not the other way round + rejected(FIELD, getter([If(bor(exact(lo("q")), Is(BOOL, fget(lo("q"), "x"), g_("k"))), + (Return(g_("r")),)), Return(g_("r"))], params=p)) + rejected(FIELD, getter([If(band(Is(BOOL, fget(lo("q"), "x"), g_("k")), exact(lo("q"))), + (Return(g_("r")),)), Return(g_("r"))], params=p)) + + +def test_the_proof_survives_a_branch_that_leaves(): + p = (obj(),) + accepted(getter([If(not_(exact(lo("q"))), (Return(g_("r")),)), Return(fget(lo("q"), "x"))], + params=p)) + # a fall-through branch does not + rejected(FIELD, getter([If(not_(exact(lo("q"))), (Assign("t", g_("r")),)), + Return(fget(lo("q"), "x"))], params=p, locals_={"t": OBJ})) + + +def test_a_fact_both_branches_establish_survives_the_join_and_one_branch_does_not(): + def join(else_value): + return getter([If(exact(lo("q")), (Assign("t", lo("q")),), (Assign("t", else_value),)), + Return(fget(lo("t"), "x"))], params=(obj(),), locals_={"t": OBJ}) + accepted(join(new(g_("a"), g_("b")))) + rejected(FIELD, join(g_("o"))) + + +def test_optional_param_is_not_none_proves_the_class(): + p = (pt(optional=True),) + accepted(getter([If(is_none(lo("p"), negate=True), (Return(fget(lo("p"), "x")),)), + Return(g_("r"))], params=p)) + accepted(getter([If(is_none(lo("p")), (Return(g_("r")),)), Return(fget(lo("p"), "x"))], + params=p)) + accepted(getter([If(is_none(lo("p")), (Return(g_("r")),), (Return(fget(lo("p"), "x")),)), + Return(g_("r"))], params=p)) + # the `None` may stand on either side + accepted(getter([If(Is(BOOL, none(), lo("p"), True), (Return(fget(lo("p"), "x")),)), + Return(g_("r"))], params=p)) + + +def test_the_none_test_proves_nothing_where_the_param_is_not_a_never_assigned_optional_class(): + rejected(FIELD, getter([If(is_none(lo("q"), negate=True), (Return(fget(lo("q"), "x")),)), + Return(g_("r"))], params=(obj(),))) + rejected(FIELD, getter([If(is_none(lo("p")), (Return(fget(lo("p"), "x")),)), Return(g_("r"))], + params=(pt(optional=True),))) # wrong sense + rejected(FIELD, getter([If(is_none(lo("p"), negate=True), (Return(fget(lo("p"), "x")),)), + Assign("p", g_("o")), Return(g_("r"))], params=(pt(optional=True),))) + # reassigned anywhere in the function -> no fact, even where the assignment is later + rejected(FIELD, getter([If(is_none(lo("p"), negate=True), (Return(fget(lo("p"), "x")),)), + Assign("p", g_("o")), Return(g_("r"))], params=(pt(optional=True),))) + # the identity of another object is not a fact about this one + rejected(FIELD, getter([If(Is(BOOL, lo("p"), g_("k"), True), (Return(fget(lo("p"), "x")),)), + Return(g_("r"))], params=(pt(optional=True),))) + + +def test_loops_kill_what_the_body_assigns(): + p = (obj(),) + guard = lambda *body: getter([If(exact(lo("q")), (While(Const(BOOL, True), tuple(body)),)), + Return(g_("r"))], params=p, pure=False) + accepted(guard(fset(lo("q"), "x", g_("v")), Break())) + # q is assigned later in the body: at the head of the next iteration the guard no longer holds + rejected(FIELD, guard(fset(lo("q"), "x", g_("v")), Assign("q", g_("o")))) + rejected(FIELD, getter([If(exact(lo("q")), (ForRange("k", Const(I64, 0), Const(I64, 3), + Const(I64, 1), ( + fset(lo("q"), "x", g_("v")), Assign("q", g_("o")))),)), Return(g_("r"))], + params=p, locals_={"k": I64}, pure=False)) + + +def test_a_proof_before_a_loop_that_assigns_does_not_outlive_it(): + p = (obj(),) + loop = ForRange("k", Const(I64, 0), Const(I64, 3), Const(I64, 1), (Assign("q", g_("o")),)) + rejected(FIELD, getter([If(exact(lo("q")), (loop, Return(fget(lo("q"), "x")))), + Return(g_("r"))], params=p, locals_={"k": I64}, pure=False)) + quiet = ForRange("k", Const(I64, 0), Const(I64, 3), Const(I64, 1), (IMPURE,)) + accepted(getter([If(exact(lo("q")), (quiet, Return(fget(lo("q"), "x")))), Return(g_("r"))], + params=p, locals_={"k": I64}, pure=False)) + + +def test_a_proof_does_not_cross_a_call_or_another_nodes_result(): + # a FieldGet result is just an object + rejected(FIELD, getter([Return(fget(fget(lo("p"), "x"), "x"))])) + rejected(FIELD, getter([Return(fget(g_("o"), "x"))])) + rejected(FIELD, getter([Return(fget(CallObject(OBJ, g_("mk"), ()), "x"))], pure=False)) + + +def test_field_set_needs_the_same_proof(): + accepted(fn(params=[pt()], pure=False, body=[fset(lo("p"), "x", g_("v")), Return(lo("p"))])) + rejected(FIELD, fn(params=[obj("p")], pure=False, body=[fset(lo("p"), "x", g_("v")), + Return(lo("p"))])) + rejected(FIELD, fn(params=[pt(optional=True)], pure=False, + body=[fset(lo("p"), "x", g_("v")), Return(lo("p"))])) + accepted(fn(params=[obj("p")], pure=False, + body=[If(exact(lo("p")), (fset(lo("p"), "x", g_("v")),)), Return(lo("p"))])) + + +def test_field_set_value_must_be_an_object(): + rejected(OWNERSHIP, fn(params=[pt()], pure=False, + body=[fset(lo("p"), "x", Const(I64, 1)), Return(lo("p"))])) + accepted(fn(params=[pt()], pure=False, body=[fset(lo("p"), "x", Box(OBJ, Const(I64, 1))), + Return(lo("p"))])) + + +# --- 3. purity and deopt ------------------------------------------------------------------------- + +def test_check_exact_in_a_pure_function_is_a_deopt_point(): + accepted(getter([Return(check(lo("q")))], params=(obj(),), may_deopt=True)) + rejected(FLAG_DEOPT, getter([Return(check(lo("q")))], params=(obj(),), may_deopt=False)) + + +def test_check_exact_in_an_impure_function_is_rejected(): + rejected(DEOPT, fn(params=[obj()], pure=False, may_deopt=False, + body=[IMPURE, Return(check(lo("q")))])) + rejected(DEOPT, fn(params=[obj()], pure=False, may_deopt=True, + body=[IMPURE, Return(check(lo("q")))])) + + +def test_a_redundant_check_exact_still_counts_as_a_deopt_point(): + rejected(DEOPT, fn(params=[pt()], pure=False, body=[IMPURE, Return(check(lo("p")))])) + + +def test_a_caller_of_a_check_exact_function_inherits_its_deopt(): + callee = fn("g", params=[obj()], may_deopt=True, body=[Return(check(lo("q")))]) + impure = fn("f", params=[obj()], pure=False, body=[IMPURE, Return(Call(OBJ, "g", (lo("q"),)))]) + rejected(DEOPT, impure, callee) + accepted(fn("f", params=[obj()], may_deopt=True, body=[Return(Call(OBJ, "g", (lo("q"),)))]), + callee) + + +def test_a_call_whose_callee_guards_a_class_param_may_deopt_unless_the_argument_is_proved(): + callee = fn("g", params=[pt()], body=[Return(fget(lo("p"), "x"))]) + unproved = fn("f", params=[obj()], may_deopt=True, body=[Return(Call(OBJ, "g", (lo("q"),)))]) + accepted(unproved, callee) + rejected(FLAG_DEOPT, dataclasses.replace(unproved, may_deopt=False), callee) + rejected(DEOPT, fn("f", params=[obj()], pure=False, body=[IMPURE, + Return(Call(OBJ, "g", (lo("q"),)))]), + callee) + # proved arguments cannot fail the callee's guard: no deopt + for arg, params in ((lo("p"), [pt()]), (new(g_("a"), g_("b")), [])): + accepted(fn("f", params=params, may_deopt=False, body=[Return(Call(OBJ, "g", (arg,)))]), + callee) + accepted(fn("f", params=[obj()], may_deopt=True, + body=[Return(Call(OBJ, "g", (check(lo("q")),)))]), callee) # the CheckExact deopts + accepted(fn("f", params=[pt()], pure=False, body=[IMPURE, Return(Call(OBJ, "g", (lo("p"),)))]), + callee) + # the wrong class proves nothing for this callee + rejected(FLAG_DEOPT, fn("f", params=[pt("p", cls="Other")], may_deopt=False, + body=[Return(Call(OBJ, "g", (lo("p"),)))]), callee) + # an optional class param is satisfied by a proved exact argument too, not by an unproved one + opt = fn("g", params=[pt(optional=True)], body=[Return(g_("a"))]) + accepted(fn("f", params=[pt()], may_deopt=False, body=[Return(Call(OBJ, "g", (lo("p"),)))]), opt) + rejected(FLAG_DEOPT, fn("f", params=[obj()], may_deopt=False, + body=[Return(Call(OBJ, "g", (lo("q"),)))]), opt) + + +def test_field_set_is_an_effect(): + rejected(FLAG_PURE, fn(params=[pt()], pure=True, body=[fset(lo("p"), "x", g_("v")), + Return(lo("p"))])) + callee = fn("g", params=[pt()], pure=False, body=[fset(lo("p"), "x", g_("v")), Return(lo("p"))]) + rejected(FLAG_PURE, fn("f", params=[pt()], pure=True, body=[Return(Call(OBJ, "g", (lo("p"),)))]), + callee) + + +def test_field_set_makes_a_function_not_closed(): + rejected(ENTRY_OPEN, fn(params=[pt()], pure=False, returns=F64, entry_globals=[Param("G", F64)], + body=[fset(lo("p"), "x", g_("v")), Return(Local(F64, "G"))])) + + +def test_reads_identity_and_allocation_are_not_effects(): + accepted(getter([Return(fget(lo("p"), "x"))])) + accepted(getter([If(is_none(lo("q")), (Return(g_("a")),)), Return(g_("b"))], + params=(obj(),))) + accepted(getter([If(exact(lo("q")), (Return(g_("a")),)), Return(g_("b"))], params=(obj(),))) + accepted(getter([Return(new(g_("a"), g_("b")))], params=())) + accepted(fn(params=[pt()], returns=F64, entry_globals=[Param("G", F64)], + body=[Assign("t", fget(lo("p"), "x")), Return(Local(F64, "G"))], + locals_={"t": OBJ})) # a field read keeps a function closed + + +# --- 4. typing ----------------------------------------------------------------------------------- + +def test_is_and_is_exact_give_bool_over_objects(): + accepted(getter([If(Is(BOOL, lo("q"), g_("k")), (Return(g_("a")),)), Return(g_("b"))], + params=(obj(),))) + rejected(TYPE, getter([If(Is(OBJ, lo("q"), g_("k")), (Return(g_("a")),)), Return(g_("b"))], + params=(obj(),))) + rejected(OWNERSHIP, getter([If(Is(BOOL, Const(I64, 1), g_("k")), (Return(g_("a")),)), + Return(g_("b"))], params=(obj(),))) + rejected(TYPE, getter([If(Is(BOOL, lo("q"), g_("k"), 1), (Return(g_("a")),)), + Return(g_("b"))], params=(obj(),))) + rejected(TYPE, getter([If(IsExact(OBJ, lo("q"), "Point"), (Return(g_("a")),)), + Return(g_("b"))], params=(obj(),))) + rejected(OWNERSHIP, getter([If(IsExact(BOOL, Const(F64, 1.0), "Point"), (Return(g_("a")),)), + Return(g_("b"))], params=(obj(),))) + + +def test_field_get_new_and_check_exact_give_obj(): + for node in (fget(lo("p"), "x"), check(lo("p")), new(g_("a"), g_("b"))): + for wrong in (I64, F64, BOOL): + rejected(TYPE, getter([Return(dataclasses.replace(node, type=wrong))], + may_deopt=isinstance(node, CheckExact), returns=wrong)) + + +def test_the_operand_of_a_field_node_must_be_an_object(): + rejected(OWNERSHIP, getter([Return(fget(Const(I64, 1), "x"))], params=())) + rejected(OWNERSHIP, getter([Return(check(Const(I64, 1)))], params=(), may_deopt=True)) + rejected(OWNERSHIP, getter([If(exact(Const(I64, 1)), (Return(g_("a")),)), Return(g_("b"))], + params=())) + + +def test_none_is_an_obj_constant_and_nothing_else_is(): + rejected(TYPE, getter([If(Is(BOOL, lo("q"), Const(OBJ, 0)), (Return(g_("a")),)), + Return(g_("b"))], params=(obj(),))) + rejected(TYPE, getter([If(Is(BOOL, lo("q"), Const(OBJ, b"x")), (Return(g_("a")),)), + Return(g_("b"))], params=(obj(),))) + # a str is the other OBJ constant (#150); its text must be exactly what the C source carries + accepted(getter([Return(Const(OBJ, "x"))], params=())) + rejected(TYPE, getter([Return(Const(OBJ, "a\0b"))], params=())) + rejected(TYPE, getter([Return(Const(OBJ, "\ud800"))], params=())) + # a None constant is a new reference to the singleton wherever an object may flow + accepted(getter([Return(none())], params=())) + accepted(getter([Assign("t", none()), Return(lo("t"))], params=(), locals_={"t": OBJ})) + accepted(getter([Return(new(none(), g_("b")))], params=())) + rejected(TYPE, getter([Return(Const(I64, None))], params=(), returns=I64)) + rejected(TYPE, getter([Return(Const(OBJ, 0))], params=())) + + +def test_a_none_constant_still_fits_no_scalar_slot(): + rejected(OWNERSHIP, getter([Return(none())], params=(), returns=F64)) + + +# --- 5. ownership -------------------------------------------------------------------------------- + +def test_new_field_get_and_check_exact_are_reference_creators(): + accepted(getter([Return(new(g_("a"), g_("b")))], params=())) + accepted(getter([Assign("t", fget(lo("p"), "x")), Return(lo("t"))], locals_={"t": OBJ})) + accepted(getter([Return(check(lo("q")))], params=(obj(),), may_deopt=True)) + + +def test_an_object_cannot_flow_into_a_scalar_slot_from_these_nodes(): + rejected(OWNERSHIP, getter([Return(fget(lo("p"), "x"))], returns=F64)) + rejected(OWNERSHIP, getter([Assign("n", fget(lo("p"), "x")), Return(li("n"))], + locals_={"n": I64}, returns=I64)) + + +# --- "nothing slips through": seeded mutation of class-using functions --------------------------- + +def _base_functions(): + pure = fn("mix", params=[pt("p"), obj("q")], locals_={"t": OBJ, "r": OBJ}, returns=OBJ, pure=True, + may_deopt=True, line=3, body=[ + Assign("t", check(lo("q"))), + Assign("r", new(fget(lo("t"), "x"), fget(lo("p"), "y"))), + If(exact(lo("q")), (Return(fget(lo("q"), "x")),)), + Return(fget(lo("r"), "y")), + ]) + impure = fn("move", params=[pt("p"), pt("o", optional=True), obj("n")], locals_={"t": OBJ}, + returns=NONE, pure=False, may_deopt=False, line=20, body=[ + IMPURE, + If(is_none(lo("o"), negate=True), + (fset(lo("o"), "x", fget(lo("p"), "y")),)), + If(not_(exact(lo("n"))), (Return(),)), + fset(lo("p"), "x", fget(lo("n"), "x")), + Assign("t", new(fget(lo("n"), "y"), g_("k"))), + fset(lo("t"), "y", lo("n")), + Return(), + ]) + return pure, impure + + +def test_the_unmutated_class_functions_are_accepted(): + for g in _base_functions(): + accepted(g) + + +def _paths(node, pred, path=()): + out = [] + if pred(node): + out.append((path, node)) + if dataclasses.is_dataclass(node) and not isinstance(node, type): + for fl in dataclasses.fields(node): + out += _paths(getattr(node, fl.name), pred, path + (fl.name,)) + elif isinstance(node, tuple): + for k, x in enumerate(node): + out += _paths(x, pred, path + (k,)) + return out + + +def _replace(node, path, new_): + if not path: + return new_ + head, rest = path[0], path[1:] + if isinstance(node, tuple): + return node[:head] + (_replace(node[head], rest, new_),) + node[head + 1:] + return dataclasses.replace(node, **{head: _replace(getattr(node, head), rest, new_)}) + + +def _classy(n): return isinstance(n, (FieldGet, FieldSet, New, IsExact, CheckExact)) + + +def _class_mutations(rng, g): + """(description, mutant, acceptable rules): each mutant breaks a class property by construction.""" + kind = rng.randrange(11) + sites = _paths(g.body, _classy) + path, node = rng.choice(sites) + if kind == 0: # a class that is not declared + return "unknown class", dataclasses.replace(g, body=_replace( + g.body, path, dataclasses.replace(node, cls="Nowhere"))), {CLASS} + if kind == 1: # a field that is not in the class + fields = _paths(g.body, lambda n: isinstance(n, (FieldGet, FieldSet))) + fpath, fnode = rng.choice(fields) + return "unknown field", dataclasses.replace(g, body=_replace( + g.body, fpath, dataclasses.replace(fnode, field="nope"))), {CLASS} + if kind == 2: # New with the wrong arity + news = _paths(g.body, lambda n: isinstance(n, New)) + npath, nnode = rng.choice(news) + args = nnode.args[:-1] if rng.random() < 0.5 else nnode.args + (g_("extra"),) + return "new arity", dataclasses.replace(g, body=_replace( + g.body, npath, dataclasses.replace(nnode, args=args))), {CLASS} + if kind == 3: # New of a class whose __init__ is not trivial + news = _paths(g.body, lambda n: isinstance(n, New)) + npath, nnode = rng.choice(news) + return "new nontrivial", dataclasses.replace(g, body=_replace( + g.body, npath, dataclasses.replace(nnode, cls="SlowInit", args=(g_("a"),)))), { + CLASS, FIELD} + if kind == 4: # CheckExact in an impure function / FieldSet in a pure one + if g.pure: + st = fset(lo("p"), "x", g_("v")) + return "fieldset in pure", dataclasses.replace(g, body=(st,) + g.body), {FLAG_PURE} + st = ExprStmt(check(lo("n"))) + return "checkexact in impure", dataclasses.replace(g, body=(IMPURE, st) + g.body[1:]), { + DEOPT} + if kind == 5: # drop the entry guard of the exact param + ps = tuple(dataclasses.replace(p, cls=None) if p.name == "p" else p for p in g.params) + return "drop param guard", dataclasses.replace(g, params=ps), {FIELD} + if kind == 6: # drop the IsExact / is-not-None guard: run the then branch unconditionally + ifs = _paths(g.body, lambda n: isinstance(n, If) and _guards(n.cond)) + ipath, inode = rng.choice(ifs) + # the then body replaces the If inside its block + block_path, idx = ipath[:-1], ipath[-1] + block = _replace_get(g.body, block_path) + new_block = block[:idx] + inode.then + block[idx + 1:] + return "drop guard", dataclasses.replace(g, body=_replace(g.body, block_path, new_block)), { + FIELD} + if kind == 7: # assign over a proved local right before its use + body = g.body + victim = "t" if g.pure else "p" + k = next(j for j, s in enumerate(body) + if _uses(s, victim) and not (isinstance(s, Assign) and s.target == victim)) + st = Assign(victim, g_("unknown")) + return "kill proof", dataclasses.replace(g, body=body[:k] + (st,) + body[k:]), {FIELD} + if kind == 8: # break the pure/may_deopt flags + if g.pure: + return "flip may_deopt", dataclasses.replace(g, may_deopt=False), {FLAG_DEOPT} + return "flip pure", dataclasses.replace(g, pure=True), {FLAG_PURE} + if kind == 9: # retype a class node + path, node = rng.choice(_paths(g.body, lambda n: isinstance(n, ir.Expr) and _classy(n))) + other = rng.choice([t for t in (I64, F64, BOOL, NONE) if t is not node.type + and (t is not OBJ)]) + if isinstance(node, IsExact): + other = rng.choice([OBJ, I64]) + return "retype", dataclasses.replace(g, body=_replace( + g.body, path, dataclasses.replace(node, type=other))), {TYPE, OWNERSHIP} + # kind 10: an operand that is a scalar + objs = _paths(g.body, lambda n: isinstance(n, (FieldGet, FieldSet, IsExact, CheckExact))) + opath, onode = rng.choice(objs) + return "scalar operand", dataclasses.replace(g, body=_replace( + g.body, opath, dataclasses.replace(onode, obj=Const(I64, 1)))), {OWNERSHIP, TYPE} + + +def _guards(cond): + return (isinstance(cond, IsExact) or isinstance(cond, Is) + or (isinstance(cond, UnaryOp) and isinstance(cond.operand, IsExact))) + + +def _replace_get(node, path): + for step in path: + node = node[step] if isinstance(node, tuple) else getattr(node, step) + return node + + +def _uses(stmt, name): + return bool(_paths(stmt, lambda n: isinstance(n, Local) and n.name == name)) + + +@pytest.mark.parametrize("seed", range(40)) +def test_every_property_breaking_class_mutation_is_rejected(seed): + rng = random.Random(seed) + base = _base_functions() + seen = set() + for _ in range(60): + g = rng.choice(base) + try: + desc, mutant, rules = _class_mutations(rng, g) + except (IndexError, StopIteration): + continue # the mutation has no site in this function + out, diags = run(mutant) + seen.add(desc) + assert mutant.name not in kept(out), f"{desc} was accepted" + assert rules & rules_for(diags, mutant.name), (desc, rules, [d.rule for d in diags]) + + +def test_all_the_mutation_kinds_are_exercised_across_the_seeds(): + kinds = set() + for seed in range(40): + rng = random.Random(seed) + for _ in range(60): + g = rng.choice(_base_functions()) + try: + kinds.add(_class_mutations(rng, g)[0]) + except (IndexError, StopIteration): + pass + assert kinds >= {"unknown class", "unknown field", "new arity", "fieldset in pure", + "checkexact in impure", "drop param guard", "drop guard", "kill proof", + "flip may_deopt", "flip pure", "retype", "scalar operand", "new nontrivial"} + + +def test_dropping_each_guard_of_the_base_functions_is_caught_one_by_one(): + pure, impure = _base_functions() + # the three guards: p's entry guard (both), q's IsExact (pure), o's is-not-None and n's early exit + for g, guard_param in ((pure, "p"), (impure, "p"), (impure, "o")): + ps = tuple(dataclasses.replace(p, cls=None, optional=False) if p.name == guard_param else p + for p in g.params) + rejected(FIELD, dataclasses.replace(g, params=ps), name=g.name) + drop_if = dataclasses.replace(pure, body=pure.body[:2] + pure.body[2].then + pure.body[3:]) + rejected(FIELD, drop_if, name="mix") + no_none_guard = dataclasses.replace( + impure, body=impure.body[:1] + impure.body[1].then + impure.body[2:]) + rejected(FIELD, no_none_guard, name="move") + no_exit = dataclasses.replace(impure, body=impure.body[:2] + impure.body[3:]) + rejected(FIELD, no_exit, name="move") diff --git a/python-multiplatform-ksp/typedpython-gate.lock.json b/python-multiplatform-ksp/typedpython-gate.lock.json new file mode 100644 index 000000000..01e3aa934 --- /dev/null +++ b/python-multiplatform-ksp/typedpython-gate.lock.json @@ -0,0 +1,19 @@ +{ + "pyrefly": { + "sha256": { + "pyrefly-1.3.2-py3-none-macosx_10_12_x86_64.whl": "626dbaee16ae1169a10a4ba3293583b5ab0320102f35efaad9e03457a2fc646c", + "pyrefly-1.3.2-py3-none-macosx_11_0_arm64.whl": "2d5fc704dbf0e990dc4f15cb1ead64f1a325ae27383a50c76cfcaf0387f926bb", + "pyrefly-1.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl": "89dbe8a33e735fa96023c33ce5dfd81af9b0bdd2ee771e8d69b2ebffc7e0fbc7", + "pyrefly-1.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl": "3ec0c31715d48bff279f548b85a93b5bc43d2c7bd1492a8cd703a9185cb089e5", + "pyrefly-1.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl": "3b3de468bed691e40dc6271284c8dcc05e649d7ed45280ef3d07332741efe34c", + "pyrefly-1.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "cb94365fe5f854cb4fe82f600c4e2453160d328610b12981004851ee0b6e8aeb", + "pyrefly-1.3.2-py3-none-musllinux_1_2_aarch64.whl": "4f730d2e515ac25827fc68f343a7aa02b1072e0062df6a430346a4ffdfb5db2f", + "pyrefly-1.3.2-py3-none-musllinux_1_2_x86_64.whl": "60f440e1626bd5cc4a41ee90cecba80796ed01ebd6c15d9f933efb8d07ffe2b8", + "pyrefly-1.3.2-py3-none-win32.whl": "b31684189de44a25a5bf00bdf3ad16be44a7b14654702d6018bce1043357202c", + "pyrefly-1.3.2-py3-none-win_amd64.whl": "693960cdb247d1f76b9e09b3f2554dae20034f7d442ccd4e9b3e8962a3622363", + "pyrefly-1.3.2-py3-none-win_arm64.whl": "f7424b21331670522980ff13e08bd143659879a4d92f4832282c7d7c41a9b5d6" + }, + "source": "https://pypi.org/pypi/pyrefly/1.3.2/json", + "version": "1.3.2" + } +} diff --git a/python-multiplatform/build.gradle.kts b/python-multiplatform/build.gradle.kts index e1f63a250..185c40c61 100644 --- a/python-multiplatform/build.gradle.kts +++ b/python-multiplatform/build.gradle.kts @@ -6,6 +6,12 @@ import java.net.URL import java.net.URLClassLoader import java.nio.charset.Charset import java.nio.file.Path +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.SimpleFileVisitor +import java.nio.file.FileVisitResult +import java.nio.file.attribute.BasicFileAttributes +import java.io.IOException import java.lang.reflect.InvocationTargetException import java.io.FileOutputStream import java.security.MessageDigest @@ -24,6 +30,26 @@ import python.multiplatform.gradle.CPythonIncludeLayout import python.multiplatform.gradle.EmbeddedPythonVersion import python.multiplatform.gradle.GenerateWasmProxyExportsTask +// Issue #227: Kotlin's File.deleteRecursively follows symbolic links and deletes what they point +// at. This walk reports a link as an entry and removes the link only. A missing path is fine. +fun deleteWithoutFollowingLinks(path: File) { + val start = path.toPath() + if (!Files.exists(start, LinkOption.NOFOLLOW_LINKS)) return + Files.walkFileTree(start, object : SimpleFileVisitor() { + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + Files.delete(file) + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { + if (exc != null) throw exc + Files.delete(dir) + return FileVisitResult.CONTINUE + } + }) +} + + plugins { alias(libs.plugins.kotlin.multiplatform) @@ -38,7 +64,7 @@ plugins { val configuredPythonVersion = project.findProperty("pythonVersion")?.toString() ?: project.rootProject.version.toString() val pythonFreeThreaded = project.findProperty("pythonFreeThreaded")?.toString()?.toBoolean() ?: false -val pbsRelease = project.findProperty("pythonBuildStandaloneRelease")?.toString() ?: "20260807" +val pbsRelease = project.findProperty("pythonBuildStandaloneRelease")?.toString() ?: "20261013" val pythonAppleSupportBuild = project.findProperty("pythonAppleSupportBuild")?.toString() ?: "b11" val pythonVersion = configuredPythonVersion @@ -630,7 +656,7 @@ val downloadPython_ios = tasks.register("downloadPython_ios") { val iosWanted = "$iosLockKey ${iosArchive.name}" if (iosExtractDir.exists() && (!iosStamp.exists() || iosStamp.readText().trim() != iosWanted)) { println("Discarding stale iOS extraction in $iosExtractDir") - iosExtractDir.deleteRecursively() + deleteWithoutFollowingLinks(iosExtractDir) } val isEmpty = iosExtractDir.list()?.isEmpty() ?: true if (isEmpty) { @@ -693,6 +719,41 @@ val generatePythonProperties by tasks.registering { } } +/** + * The dev agent (SPEC L-12, #233) is Python, kept as a real file -- `src/commonMain/python/ + * python_multiplatform_devagent.py`, which `src/test/python/test_devagent.py` tests with plain CPython -- and + * embedded here as a Kotlin string constant, the way the runtime's other helper sources are, so every platform + * carries it with no resource or packaging step. + */ +val generateDevAgentSource by tasks.registering { + val source = layout.projectDirectory.file("src/commonMain/python/python_multiplatform_devagent.py") + val outputDir = layout.buildDirectory.dir("generated/devagent/kotlin") + inputs.file(source) + outputs.dir(outputDir) + doLast { + val escaped = buildString { + source.asFile.readText().forEach { c -> + when (c) { + '\\' -> append("\\\\") + '"' -> append("\\\"") + '$' -> append("\\$") + '\n' -> append("\\n") + '\r' -> {} + else -> append(c) + } + } + } + val file = outputDir.get().asFile.resolve("python/multiplatform/env/DevAgentSource.kt") + file.parentFile.mkdirs() + file.writeText( + "// Generated by :python-multiplatform:generateDevAgentSource from\n" + + "// src/commonMain/python/python_multiplatform_devagent.py. Do not edit.\n" + + "package python.multiplatform.env\n\n" + + "internal const val DEV_AGENT_SOURCE: String = \"" + escaped + "\"\n", + ) + } +} + val androidBuildDir = "$projectDir/build/android" // ================================================================================================= @@ -1277,7 +1338,9 @@ kotlin { sourceSets { val jvmMain by creating - val commonMain by getting + val commonMain by getting { + kotlin.srcDir(generateDevAgentSource) + } val commonTest by getting { dependencies { implementation(libs.kotlin.test) @@ -1390,7 +1453,7 @@ kotlin { } /** - * The iOS `Python.framework` ships only the interpreter binary and headers — it carries no + * The iOS `Python.framework` ships only the interpreter binary and headers, it carries no * standard library. `Py_Initialize()` therefore aborts the process with * "Fatal Python error: Failed to import encodings module" unless PYTHONHOME points at a * prefix containing `lib/python3.13`. @@ -1425,7 +1488,7 @@ tasks.withType() @@ -1925,7 +1988,7 @@ listOf("Arm64" to "arm64-v8a", "X64" to "x86_64").forEach { (targetSuffix, abi) // Ordering comes free from ES modules: the import-object module imports `cpython.mjs`, which has a // top-level `await`, so Emscripten is fully instantiated before the import object is built. // -// The CPython build itself is produced by `tools/wasm/build-cpython.sh` (also reachable as the +// The CPython build itself is produced by `python-multiplatform/scripts/wasm/build-cpython.sh` (also reachable as the // `buildWasmPython` task below) -- CPython 3.14.2 matched to `pyemscripten_2026_0` (PEP 783), // relinked with `wasmExports,wasmMemory` added to `-sEXPORTED_RUNTIME_METHODS`. Without those two the // 8287 wasm exports are present in the binary but unreachable from JS, so there is nothing to hand @@ -1951,12 +2014,12 @@ val wasmPythonDir: String = (project.findProperty("wasmPythonDir")?.toString() val buildWasmPython by tasks.registering(Exec::class) { group = "python" description = "Builds CPython 3.14.2 for wasm32-emscripten (pyemscripten_2026_0) into .caches/" - val script = rootProject.layout.projectDirectory.file("tools/wasm/build-cpython.sh").asFile + val script = rootProject.layout.projectDirectory.file("python-multiplatform/scripts/wasm/build-cpython.sh").asFile commandLine("bash", script.absolutePath, "all") } /** - * Where `tools/wasm/build-cpython.sh wheels` puts the pinned compiled wheels the wasm suite loads + * Where `python-multiplatform/scripts/wasm/build-cpython.sh wheels` puts the pinned compiled wheels the wasm suite loads * (`wasmJsTest/.../WasmCompiledWheelTest`). Not checked in: they are downloaded by URL and accepted * only against the sha256 the script pins. */ @@ -1968,7 +2031,7 @@ val wasmWheelsDir: String = (project.findProperty("wasmWheelsDir")?.toString() val fetchWasmWheels by tasks.registering(Exec::class) { group = "python" description = "Downloads the pinned pyemscripten_2026_0 wheels the wasm suite loads into .caches/" - val script = rootProject.layout.projectDirectory.file("tools/wasm/build-cpython.sh").asFile + val script = rootProject.layout.projectDirectory.file("python-multiplatform/scripts/wasm/build-cpython.sh").asFile environment("WASM_WHEELS_DIR", wasmWheelsDir) commandLine("bash", script.absolutePath, "wheels") } @@ -2043,7 +2106,7 @@ fun Task.wasmRuntimePresent(marker: File): Boolean { val remedy = "Unpack the published runtime " + "(io.github.thisisthepy:python-multiplatform-wasm-runtime:$libraryVersion) and point " + "-PwasmPythonDir / PMP_PYTHON_DIR at it, or build one into the default location with " + - "./gradlew :python-multiplatform:buildWasmPython (tools/wasm/build-cpython.sh)." + "./gradlew :python-multiplatform:buildWasmPython (python-multiplatform/scripts/wasm/build-cpython.sh)." val what = "$name: no CPython Emscripten build at ${marker.parentFile} " + "(looked for ${marker.name})." if (requireWasmRuntime) { @@ -2177,7 +2240,7 @@ val wasmBrowserRuntimeZip by tasks.registering(Zip::class) { * * Byte-identical in intent to the two substitutions the `KotlinJsTest` block below performs on this * module's own test bundle, and it exists as a function because a *consumer* has to perform them - * too — on its own webpack context, against its own module name. Neither can be done from inside + * too, on its own webpack context, against its own module name. Neither can be done from inside * Kotlin: one is the compiler's `intrinsics.memory` placeholder, the other needs a value * (`wasmInstance.exports`) that exists only in the generated entry module's scope. * @@ -2233,11 +2296,11 @@ fun patchKotlinWasmOutputForCPython(dir: File, modulePrefix: String, logger: org * The entry module with `pmpSetKotlinExports(exports)` inserted, and the *position* is the point. * * This wiring was written against a test bundle, whose entry module ends at - * `setWasmExports(wasmExports)` — the runner calls `startUnitTests` later, from outside — so + * `setWasmExports(wasmExports)`, the runner calls `startUnitTests` later, from outside, so * appending the handoff was enough. An **executable** bundle does not end there: * `binaries.executable()` makes the generated entry module finish with `exports._start()`, which is * Kotlin `main()`. Appended text therefore ran *after* the whole application had already executed, - * and every upcall in it failed with `pmpRegisterUpcall returned -1` — the code for "`kotlinExports` + * and every upcall in it failed with `pmpRegisterUpcall returned -1`, the code for "`kotlinExports` * is still null". * * Observed in a browser rather than reasoned about: the sample printed sections 1-4 correctly and @@ -2560,7 +2623,7 @@ tasks.withType().co logger.lifecycle( "No wheels in $wasmWheelsDir -- WasmCompiledWheelTest will " + (if (requireWasmRuntime) "FAIL (-PrequireWasmRuntime)" else "skip") + - ". Fetch them with tools/wasm/build-cpython.sh wheels (or :python-multiplatform:fetchWasmWheels)." + ". Fetch them with python-multiplatform/scripts/wasm/build-cpython.sh wheels (or :python-multiplatform:fetchWasmWheels)." ) } dir.resolve("cpython-config.mjs").appendText( @@ -2744,8 +2807,8 @@ publishing { url.set("https://github.com/thisisthepy/python-multiplatform-mobile") licenses { license { - //name.set("The Apache License, Version 2.0") - //url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") } } developers { diff --git a/tools/wasm/build-cpython.sh b/python-multiplatform/scripts/wasm/build-cpython.sh similarity index 95% rename from tools/wasm/build-cpython.sh rename to python-multiplatform/scripts/wasm/build-cpython.sh index 3f7ee669f..de8cd3da2 100755 --- a/tools/wasm/build-cpython.sh +++ b/python-multiplatform/scripts/wasm/build-cpython.sh @@ -2,12 +2,12 @@ # CPython 3.14.2 for wasm32-emscripten, ABI-matched to pyemscripten_2026_0 (PEP 783), and staged as # the runtime python-multiplatform's wasmJs target runs against. # -# tools/wasm/build-cpython.sh build + zip + verify + stage (needs network the first time) -# tools/wasm/build-cpython.sh zip rebuild only the stdlib zip of an existing build (no emsdk) -# tools/wasm/build-cpython.sh verify check an existing build and its zip carry the ABI claims -# tools/wasm/build-cpython.sh stage copy python.wasm, python.mjs and the zip to the runtime dir -# tools/wasm/build-cpython.sh wheels download the pinned compiled wheels the wasm suite loads -# tools/wasm/build-cpython.sh stock build a STOCK PEP 776 interpreter as a negative control +# python-multiplatform/scripts/wasm/build-cpython.sh build + zip + verify + stage (needs network the first time) +# python-multiplatform/scripts/wasm/build-cpython.sh zip rebuild only the stdlib zip of an existing build (no emsdk) +# python-multiplatform/scripts/wasm/build-cpython.sh verify check an existing build and its zip carry the ABI claims +# python-multiplatform/scripts/wasm/build-cpython.sh stage copy python.wasm, python.mjs and the zip to the runtime dir +# python-multiplatform/scripts/wasm/build-cpython.sh wheels download the pinned compiled wheels the wasm suite loads +# python-multiplatform/scripts/wasm/build-cpython.sh stock build a STOCK PEP 776 interpreter as a negative control # # Everything lands inside this repository, under the git-ignored `.caches/`: # @@ -53,7 +53,7 @@ # PYVERSION ?= 3.14.2). set -euo pipefail -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" CACHES="$ROOT/.caches" CPYTHON_TAG=v3.14.2 diff --git a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/PythonInstrumentationRunner.kt b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/PythonInstrumentationRunner.kt index 45d65ab22..4becab007 100644 --- a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/PythonInstrumentationRunner.kt +++ b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/PythonInstrumentationRunner.kt @@ -9,12 +9,12 @@ import python.native.ffi.PythonOnDevice * `commonTest` is platform-agnostic by construction: its fixture reaches straight for * `Python3.initialize()`. On desktop and the iOS simulator that is enough, because the build * hands those runs a `PYTHONHOME` with a real standard library behind it. On Android the stdlib - * lives inside the APK as assets and has to be unpacked to app-private storage first — without + * lives inside the APK as assets and has to be unpacked to app-private storage first, without * that, `Py_Initialize()` does not fail, it *aborts the process* with "Failed to import encodings * module", taking every other test on the device with it. * * Doing it in the runner rather than in a `@Before` keeps `commonTest` free of Android knowledge, - * which it has to be — it also compiles for iOS and desktop. + * which it has to be, it also compiles for iOS and desktop. * * [PythonOnDevice.ensureInitialised] is idempotent, so the instrumented tests that call it * themselves are unaffected. @@ -22,14 +22,14 @@ import python.native.ffi.PythonOnDevice class PythonInstrumentationRunner : AndroidJUnitRunner() { /** - * `onStart` runs on the instrumentation thread — the same thread that then executes the + * `onStart` runs on the instrumentation thread, the same thread that then executes the * tests. * * This used to say that mattered because `Py_Initialize()` leaves the GIL held by its caller, * so initialising here "keeps the GIL on the thread that is about to use it". That was the * pre-§1 contract and it is now exactly backwards: the GIL must **not** stay held, or no * cleaner thread can ever attach to release a reference. `PythonOnDevice.ensureInitialised` - * parks the thread state, and every entry into Python — including the ones on this thread — + * parks the thread state, and every entry into Python, including the ones on this thread, * goes through `withGIL`. * * What still matters about the thread is only that the staging and the first C API call diff --git a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/assembled/AssembledApiTest.kt b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/assembled/AssembledApiTest.kt index 468f3cf8b..c2c5b17c8 100644 --- a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/assembled/AssembledApiTest.kt +++ b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/assembled/AssembledApiTest.kt @@ -18,7 +18,7 @@ import python.native.ffi.PythonOnDevice * ### Why this file exists * * Android's instrumented tests all reach through `bindings` and rebuild each call by hand, and - * `commonTest` — which does exercise the object model — only runs on iOS and desktop. Nothing + * `commonTest`, which does exercise the object model, only runs on iOS and desktop. Nothing * ran the real API on a device, so nothing noticed that it does not work there: `Python3.exec` * crashes the process on its **first** call, on both API 26 and API 36, because * `PyImport_AddModuleRef`, `PyObject_GetAttrString` and `PyRun_String` still take a Kotlin @@ -29,7 +29,7 @@ import python.native.ffi.PythonOnDevice * * ### Why it is disabled * - * These crash the instrumentation **process**, not just the test — a crashed runner takes every + * These crash the instrumentation **process**, not just the test, a crashed runner takes every * other test on the device with it, so leaving them enabled would hide the suite rather than * report a failure. * diff --git a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/env/PythonPayloadStagingTest.kt b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/env/PythonPayloadStagingTest.kt index c062d50fc..7631c32c1 100644 --- a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/env/PythonPayloadStagingTest.kt +++ b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/multiplatform/env/PythonPayloadStagingTest.kt @@ -17,7 +17,7 @@ import kotlin.test.assertTrue * `assets/python/` is a *consumer's* asset: `toolchain`'s `stagePythonBundleAndroid` registers a * staging root into the consuming application's AGP asset sources. This library's own instrumented * APK is not such a consumer and carries no payload, so the copy loop in - * [PythonBootstrap.stagePayload] is unreachable from here — there is no fixture to drive it with, + * [PythonBootstrap.stagePayload] is unreachable from here, there is no fixture to drive it with, * and manufacturing one means adding an asset source directory to `build.gradle.kts`, which is a * build-structure change and not this test's to make. * diff --git a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniOverheadBenchmark.kt b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniOverheadBenchmark.kt index 9b12d7bbe..d6f143efc 100644 --- a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniOverheadBenchmark.kt +++ b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniOverheadBenchmark.kt @@ -64,7 +64,7 @@ class JniOverheadBenchmark { /** * The echo probes touch no interpreter state, but `realCPythonCallByCallingConvention` calls * CPython for real, and the main thread state is parked (see - * [PythonOnDevice.ensureInitialised]) — so this thread has to attach for itself. Done once + * [PythonOnDevice.ensureInitialised]), so this thread has to attach for itself. Done once * per test, outside the timed loops, so the attach is not what gets measured. */ private var gilState = 0 diff --git a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniWiringTest.kt b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniWiringTest.kt index 47d37eccf..701d32829 100644 --- a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniWiringTest.kt +++ b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/JniWiringTest.kt @@ -11,7 +11,7 @@ import org.junit.runner.RunWith * Settles, on a real device, whether the JNI wiring between the Kotlin/Native `@CName` exports and * the `external fun` declarations in [bindings] actually passes arguments correctly. * - * The exports take only their declared arguments — none of them accepts a `JNIEnv*`. But `bindings` + * The exports take only their declared arguments, none of them accepts a `JNIEnv*`. But `bindings` * declares them as ordinary JNI methods, and ART invokes those as * `f(JNIEnv*, jobject, args...)`. If that is what happens, every argument arrives shifted by two * slots and the first one reads the `JNIEnv` pointer instead of its value. @@ -57,13 +57,13 @@ class JniWiringTest { @Test fun zeroArgCallsWorkRegardless() { - // Expected to pass even with a broken convention — recorded so the contrast with + // Expected to pass even with a broken convention, recorded so the contrast with // argumentsArriveUnshifted is visible in the results. // // This used to assert 0, "before initialisation". That premise died when commonTest was // connected to Android: 176 tests share one process, and whether the interpreter is // already up when this runs is decided by class ordering, not by wiring. It failed on - // both API levels for exactly that reason — the interpreter was already up, so it read 1. + // both API levels for exactly that reason, the interpreter was already up, so it read 1. // // Pinning the interpreter to a known state first and asserting the exact value is the // stronger check anyway: a zero-arg call under a shifted convention returns whatever diff --git a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/PythonOnDevice.kt b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/PythonOnDevice.kt index b3a0cde41..3f0703647 100644 --- a/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/PythonOnDevice.kt +++ b/python-multiplatform/src/androidInstrumentedTest/kotlin/python/native/ffi/PythonOnDevice.kt @@ -35,8 +35,8 @@ object PythonOnDevice { * Calling the C function directly here skipped the parking, and then made the omission * permanent: `Python3.isInitialized` is seeded from `Py_IsInitialized()` the first time the * object is touched, so it latched to `true` and [Python3.initialize] returned early for the - * rest of the process. The instrumentation thread — which is also the thread every test body - * runs on — held the GIL for the entire run, and every cleaner blocked on the first + * rest of the process. The instrumentation thread, which is also the thread every test body + * runs on, held the GIL for the entire run, and every cleaner blocked on the first * `PyGILState_Ensure` it reached. * * That state is invisible while nothing releases the GIL, which is why it survived: the old diff --git a/python-multiplatform/src/androidMain/README.md b/python-multiplatform/src/androidMain/README.md index 5e6a03298..94313778c 100644 --- a/python-multiplatform/src/androidMain/README.md +++ b/python-multiplatform/src/androidMain/README.md @@ -1,4 +1,4 @@ -# androidMain — rules +# androidMain, rules The JVM half of Android. Reaches CPython through JNI into the Kotlin/Native library built from `artMain` + `nativeMain`. @@ -6,9 +6,9 @@ The JVM half of Android. Reaches CPython through JNI into the Kotlin/Native libr ## The host app's bootstrap belongs here, not in the README `PythonBootstrap` unpacks the stdlib out of the AAR's assets, sets `PYTHONHOME` and calls -`Python3.initialize`. It exists because the same ~30–55 lines had been hand-written three times — +`Python3.initialize`. It exists because the same ~30–55 lines had been hand-written three times, `sample`'s `MainActivity`, the external consumer app of ROADMAP §15d, and this repo's own -`PythonOnDevice` fixture — from one README paragraph, and **all three got the same thing wrong**: +`PythonOnDevice` fixture, from one README paragraph, and **all three got the same thing wrong**: each decided "already unpacked" by probing one entry of the result, which a copy interrupted partway through satisfies. See ROADMAP §15f. @@ -22,7 +22,7 @@ Two rules follow for anything added here: before a rewrite starts, so a partial tree cannot look complete. `encodings/ exists` is a property of a half-finished copy too. -`AssetManager.open()` throwing `FileNotFoundException` is how a directory is detected — it folds +`AssetManager.open()` throwing `FileNotFoundException` is how a directory is detected, it folds the classification into the `open()` the file needed anyway and is worth ~2x over a `list()` per entry (192→124 ms on API 26, 324→150 ms on API 36, for 804 files / 20.1 MB). It is **not** in `AssetManager`'s documented contract, and AssetManager was reimplemented wholesale in API 28, so @@ -31,7 +31,7 @@ whatever API level is running rather than trusting it. ## The consumer's payload is a second tree, with a second stamp -`assets/python/` is the consumer's own Python — `toolchain`'s `stagePythonBundleAndroid` registers +`assets/python/` is the consumer's own Python, `toolchain`'s `stagePythonBundleAndroid` registers its staging root as an AGP asset source. `PythonBootstrap.stagePayload` unpacks it to `/python` and registers it; `Python3.initialize` puts it on `sys.path[0]`. @@ -57,18 +57,18 @@ steps: `PythonBootstrap` does the half that needs a `Context`, exactly as it alr declarations here. Two reasons, both measured: - Name-based linking of a `@CriticalNative` method **aborts the ART runtime** below Android 12 - (`zygote64: runtime.cc:492] Runtime aborting...`), and is merely slow above it — 76.42 ns + (`zygote64: runtime.cc:492] Runtime aborting...`), and is merely slow above it, 76.42 ns against 46.55 ns for the registered form on API 36 hardware. - Registration removes the dependence on JNI name mangling, which is what produced the original argument-shift bug in the first place. -### Every `external fun` here needs a table entry — no exceptions by reachability +### Every `external fun` here needs a table entry, no exceptions by reachability An unregistered declaration does not fail cleanly when it is finally reached. It falls back to a `@CName` export in `nativeMain` that has no `JNIEnv*`/`jclass` prologue, so the arguments arrive shifted by two registers, and the process dies on a truncated pointer (ROADMAP §2 records two: `PyImport_AddModule` at `0xc24110e8`, `PyErr_GetRaisedException` at `0xc2411160`). "Nothing calls -it today" is not a defence — it is a description of when the crash happens, not whether. +it today" is not a defence, it is a description of when the crash happens, not whether. There are exactly four deliberate exceptions and they are named in ROADMAP §2. Adding a fifth means writing down why. @@ -85,7 +85,7 @@ Two mechanical rules fall out of this: grep -oE "^static [a-z]+ (f_[A-Za-z0-9_]+)" jni_onload.def | awk '{print $3}' | sort | uniq -d ``` -To audit the whole surface, join the compiled class against the table — the class, not the Kotlin +To audit the whole surface, join the compiled class against the table, the class, not the Kotlin source, because the descriptor the JVM will match on is the one in the bytecode: ```bash @@ -96,17 +96,17 @@ javap -p -s build/tmp/kotlin-classes/debug/python/native/ffi/bindings.class Two independent axes. Correctness first. -**Axis 1 — what the function may do.** `@FastNative` and `@CriticalNative` both stop the +**Axis 1, what the function may do.** `@FastNative` and `@CriticalNative` both stop the collector for the duration of the call, and `@CriticalNative` has no `JNIEnv`, so nothing under -it can re-enter the runtime. Anything that can execute arbitrary Python — a module's top-level -code during import, a `__getattr__`, a `__del__` reached by dropping the last reference — +it can re-enter the runtime. Anything that can execute arbitrary Python, a module's top-level +code during import, a `__getattr__`, a `__del__` reached by dropping the last reference, stays on ordinary JNI at every API level. `PyErr_Clear` and `PyObject_GetAttrString` both look like leaves and are not. When in doubt, ordinary JNI. A wrong guess that way costs nanoseconds; the other way is a crash once upcalls exist. -**Axis 2 — what the device fast-paths.** Measured across five API levels, net of the Kotlin +**Axis 2, what the device fast-paths.** Measured across five API levels, net of the Kotlin floor, ns/call: | API | `@CriticalNative` | `@FastNative` | ordinary JNI | @@ -122,7 +122,7 @@ Hence `bindings.preferFastNative = SDK_INT >= 34`. `dispatchPicksTheFasterConven re-measures both on whatever device runs it and fails if the flag disagrees, so a future release moving the boundary shows up as a test failure rather than silent slowness. -Read the table in the direction that matters when adding a call site — how much a promotion is +Read the table in the direction that matters when adding a call site, how much a promotion is worth, relative to leaving it ordinary: | API | ordinary → `@FastNative` | ordinary → `@CriticalNative` | @@ -146,7 +146,7 @@ Ordinary JNI is the default. A promotion has to survive all five steps. Any refcount reaching zero deallocates and runs `__del__`, which is arbitrary Python. This catches far more than the names suggest: every setter that overwrites (`PyTuple_SetItem`, `PyList_SetItem`, -`PyDict_SetItem*`, `PySys_SetObject`), every delete, `PyErr_Clear`, `Py_Finalize` — and +`PyDict_SetItem*`, `PySys_SetObject`), every delete, `PyErr_Clear`, `Py_Finalize`, and `PyGILState_Release`, because the release that drops the counter to zero deletes the thread state, and clearing a thread state decrefs its dict and exception state. A function that looks pure and touches a refcount is re-entrant. @@ -159,7 +159,7 @@ and descriptor `__get__` on any attribute access, `__hash__`/`__eq__` on any dic *including lookups*, `__iter__`/`__next__` inside `PySequence_Fast`, `__getitem__` inside `PyUnicode_Translate`, module top-level code on any import, a Python codec on any encode/decode that takes an encoding name, and `warnings.showwarning` inside `PyErr_WarnExplicit`. -`PyThreadState_GetDict` belongs here too — it allocates the thread dict lazily on first call per +`PyThreadState_GetDict` belongs here too, it allocates the thread dict lazily on first call per thread, and is not the struct read its name implies. **3. Can it block?** → ordinary. `PyGILState_Ensure`, `PyEval_RestoreThread`. A thread waiting for @@ -171,7 +171,7 @@ this call. There is no C API function that cannot fail, and CPython reports failure by constructing an exception instance, which is GC-tracked, which lands back in step 2. So step 4 never returns -"provably a leaf" — it returns "leaf on the success path, second-order re-entrant on the error +"provably a leaf", it returns "leaf on the success path, second-order re-entrant on the error path". That is why the promoted set is short and enumerated rather than derived from a rule: it is the set where a benchmark showed the convention *is* the call cost, not the set that passed an argument. @@ -182,7 +182,7 @@ The C wrapper for the `@FastNative` twin is the same body with a leading `JNIEnv ### The five steps are enforced, not just written down -`JniCallConventionClassificationTest` (desktopTest — it reads sources, so it needs no device) +`JniCallConventionClassificationTest` (desktopTest, it reads sources, so it needs no device) re-derives the classification on every run from `bindings.kt`, `artMain/cinterop/jni_onload.def` and the CPython headers bundled in `src/nativeInterop/cinterop/include`, and fails if a call site reaches a GC-blocking binding that is not in its enumerated promoted set. @@ -190,11 +190,11 @@ a GC-blocking binding that is not in its enumerated promoted set. Two things it is deliberately not: - **It does not decide promotions.** The promoted set is a `Map` in the test with a reason per entry, - because step 4 has no derivable answer — every C API function can fail, and failure allocates a + because step 4 has no derivable answer, every C API function can fail, and failure allocates a GC-tracked exception. What the test enforces is that the set stays *enumerated*: a new `@FastNative`/`@CriticalNative` declaration fails the build until someone writes down why. -- **It does not claim to prove leafness.** A header prototype can condemn a function — a `PyObject *` - crossing it means a decref, a slot, or a GC-tracked allocation is reachable — but nothing in the +- **It does not claim to prove leafness.** A header prototype can condemn a function, a `PyObject *` + crossing it means a decref, a slot, or a GC-tracked allocation is reachable, but nothing in the headers clears one. `Py_Initialize` and `PyRun_SimpleString` mention no `PyObject *` at all and both run arbitrary Python, so everything the rule cannot condemn lands in an **UNDECIDED** bucket that is barred from promotion. The bucket is currently 65 of 366 registrations. The audit that reported @@ -208,21 +208,21 @@ holds 366 today. | guessed wrong toward | costs | |---|---| -| ordinary | 5.8 ns (API 33/34) to 41 ns (API 30) per call — measurable, and bounded | +| ordinary | 5.8 ns (API 33/34) to 41 ns (API 30) per call, measurable, and bounded | | a promotion | the collector stalls for as long as the Python runs; under `@CriticalNative` there is no `JNIEnv`, so any upcall that Python reaches has none either | Only the first of those shows up in a test. The second needs a device, live upcalls, *and* the -cyclic collector to fire inside that particular call — so it is found in production, not in CI. +cyclic collector to fire inside that particular call, so it is found in production, not in CI. ## The boundary carries primitives only -`Long`, `Int`, `Double`, `Unit`. No `String`, no nullable `JNIPointer?` — a nullable `Long` is a +`Long`, `Int`, `Double`, `Unit`. No `String`, no nullable `JNIPointer?`, a nullable `Long` is a boxed `java.lang.Long` on the JVM against a raw `jlong` on the other side. Strings cross as addresses; null pointers cross as `0L`. Conversion happens here, in `EmbedAPI.android.kt`. ## String marshalling is the expensive part, not crossings -Composing `Python3.exec` natively nearly halves it — 17.8 µs to 10.1 µs. That is **not** the +Composing `Python3.exec` natively nearly halves it, 17.8 µs to 10.1 µs. That is **not** the crossing count: 7.7 µs over ~11 crossings would be 700 ns each, against the ~7 ns a crossing actually costs, and the per-call figure is identical on API 26 and API 36 despite a 20x difference in per-crossing cost. It is the three `ffiAllocUtf8` round trips, each a JNI call @@ -245,7 +245,7 @@ libcore's `System.gc()` records a request and defers the collection until the ne `System.runFinalization()`, so a loop of bare `System.gc()` calls runs **no collection at all**. On HotSpot the same loop collects, which is why the desktop suite never saw this. -Anything on Android that waits for the collector — every GC-driven release test — must use +Anything on Android that waits for the collector, every GC-driven release test, must use `Runtime.getRuntime().gc()` followed by `System.runFinalization()`. The whole of `GCLeakTest` failed on both API levels for this reason, and the failure reads as "no cleaner ran", which points at the cleaner threads rather than at the collector that never started. @@ -257,7 +257,7 @@ thread state with `PyEval_SaveThread()` immediately afterwards, and that parking cleaner thread attach through `PyGILState_Ensure` and call `Py_DecRef` (ROADMAP §1). `PythonInstrumentationRunner` used to bring the interpreter up with a bare `Py_Initialize()`, -which skipped the parking — and then made the omission permanent, because `Python3.isInitialized` +which skipped the parking, and then made the omission permanent, because `Python3.isInitialized` is seeded from `Py_IsInitialized()` when the object is first touched, so it latched to `true` and `Python3.initialize()` returned early for the rest of the process. The instrumentation thread, which is also the thread every test body runs on, held the GIL for the whole run. @@ -269,6 +269,6 @@ Two consequences worth knowing, because neither looks like a GIL problem: gets stuck **during** the test; one stuck in an earlier test is indistinguishable from "nothing was collected". - Instrumented tests that reach `bindings` directly ran fine only because that thread happened - to hold the GIL. Once it is parked they must attach for themselves — + to hold the GIL. Once it is parked they must attach for themselves, `PythonOnDevice.attach()`/`detach()` in a `@Before`/`@After` pair, which is what `CompositionBenchmark`, `JniOverheadBenchmark` and `JniWiringTest` now do. diff --git a/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonBootstrap.kt b/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonBootstrap.kt index 75b418557..9ad7a18ae 100644 --- a/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonBootstrap.kt +++ b/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonBootstrap.kt @@ -147,7 +147,7 @@ object PythonBootstrap { } stampFile.delete() - payloadDir.deleteRecursively() + deleteWithoutFollowingLinks(payloadDir) if (!payloadDir.mkdirs() && !payloadDir.isDirectory) { throw IOException("could not create $payloadDir to unpack the Python payload into") } @@ -225,7 +225,7 @@ object PythonBootstrap { stampFile(prefix).delete() // Whatever is there is either from another build or unfinished; neither can be merged // into safely, because a file that used to exist and no longer does would survive. - stdlibDir.deleteRecursively() + deleteWithoutFollowingLinks(stdlibDir) if (!stdlibDir.mkdirs() && !stdlibDir.isDirectory) { throw IOException("could not create $stdlibDir to unpack the CPython standard library into") } diff --git a/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonPayload.android.kt b/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonPayload.android.kt index 2c4045920..08e805dfe 100644 --- a/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonPayload.android.kt +++ b/python-multiplatform/src/androidMain/kotlin/python/multiplatform/env/PythonPayload.android.kt @@ -14,7 +14,7 @@ import java.io.File * * ### Why discovery here is a registry rather than a scan * - * Unpacking assets needs a `Context`, and `Python3.initialize()` has none — it is a `commonMain` + * Unpacking assets needs a `Context`, and `Python3.initialize()` has none, it is a `commonMain` * function on an object with no platform state. The two-step (stage, then register; discover, then * install) is what lets `PythonBootstrap.initialize(context)` do the part that needs a `Context` * before the part that does not. `PythonBootstrap` already sits in exactly that position for diff --git a/python-multiplatform/src/androidMain/kotlin/python/multiplatform/ref/PyAutoCloseable.android.kt b/python-multiplatform/src/androidMain/kotlin/python/multiplatform/ref/PyAutoCloseable.android.kt index c2ebe6f99..f3a20a979 100644 --- a/python-multiplatform/src/androidMain/kotlin/python/multiplatform/ref/PyAutoCloseable.android.kt +++ b/python-multiplatform/src/androidMain/kotlin/python/multiplatform/ref/PyAutoCloseable.android.kt @@ -16,7 +16,7 @@ actual interface PlatformCleaner : AutoCloseable { * [PhantomCleanerRegistry], which is the same mechanism written by hand. * * This nullable value, not `SDK_INT`, is what the code below branches on. The two are equivalent - * only as long as nothing else can make the `Cleaner` absent — and the previous version of this + * only as long as nothing else can make the `Cleaner` absent, and the previous version of this * file branched on `SDK_INT` and then called `sharedCleaner?.register(...)`, so a null here on * API 33+ would have produced a wrapper with **no release registered at all**, silently, with the * `?.` reading as caution. Branching on the object removes the case rather than tolerating it. @@ -32,11 +32,11 @@ actual fun registerCleaner( } /** - * One release, run by whichever of `close()` and the collector gets there first — and never by + * One release, run by whichever of `close()` and the collector gets there first, and never by * both. Both branches guarantee that: `Cleaner.Cleanable.clean()` is documented to run its action * at most once, and [PhantomCleanerRegistry.Cleanable] holds a compare-and-set flag for it. It has * to be a guarantee rather than a convention, because a second run is a second `Py_DecRef` on a - * reference this wrapper no longer owns — the double free of ROADMAP §1 and §4. + * reference this wrapper no longer owns, the double free of ROADMAP §1 and §4. */ private class AndroidCleaner( pointer: NativePointer, diff --git a/python-multiplatform/src/androidMain/kotlin/python/native/ffi/EmbedAPI.android.kt b/python-multiplatform/src/androidMain/kotlin/python/native/ffi/EmbedAPI.android.kt index 45d6130c2..5b6c42d3d 100644 --- a/python-multiplatform/src/androidMain/kotlin/python/native/ffi/EmbedAPI.android.kt +++ b/python-multiplatform/src/androidMain/kotlin/python/native/ffi/EmbedAPI.android.kt @@ -16,8 +16,8 @@ actual fun AddressValue.toNativePointer(): NativePointer = NativePointer((this a * Deliberately does not delegate to the `JNIPointer?` overload below. * * `JNIPointer` is a typealias for `Long`, so that overload's receiver is `Long?` and this one's is - * `Long`. Overload resolution prefers the more specific, non-null receiver — that is, this function - * itself — so writing `toNativePointer()` here recurses forever and blows the stack. On Android the + * `Long`. Overload resolution prefers the more specific, non-null receiver, that is, this function + * itself, so writing `toNativePointer()` here recurses forever and blows the stack. On Android the * platform pointer *is* the `Long`, so the conversion is written out instead. */ @HighOverheadNativeCall diff --git a/python-multiplatform/src/androidMain/kotlin/python/native/ffi/UpcallEntry.android.kt b/python-multiplatform/src/androidMain/kotlin/python/native/ffi/UpcallEntry.android.kt index 350f4e300..885cc6702 100644 --- a/python-multiplatform/src/androidMain/kotlin/python/native/ffi/UpcallEntry.android.kt +++ b/python-multiplatform/src/androidMain/kotlin/python/native/ffi/UpcallEntry.android.kt @@ -57,11 +57,11 @@ import python.multiplatform.reflection.UpcallTable * `threading.Thread` is a bare pthread, so the first time one reaches ART it must be attached. * `pmp_attach` holds that attachment for the life of the thread and gives it back from a * `pthread_key_create` destructor, which is what turned it from a per-call charge of 14–62 µs into - * a per-thread one — but a thread that upcalls exactly once still pays all of it. + * a per-thread one, but a thread that upcalls exactly once still pays all of it. * * It is not marked with [HighOverheadNativeCall]. That marker is `@RequiresOptIn`, and its job is - * to make a Kotlin *caller* acknowledge a cost; nothing in Kotlin calls these — C does, from - * CPython — so applying it here would be inert. The cost is recorded instead where the callers + * to make a Kotlin *caller* acknowledge a cost; nothing in Kotlin calls these, C does, from + * CPython, so applying it here would be inert. The cost is recorded instead where the callers * that can act on it will look: here, in `pmp_upcall_invoke_meth`, and in `docs/design/upcall.md`. */ object UpcallCallbacks { diff --git a/python-multiplatform/src/artMain/README.md b/python-multiplatform/src/artMain/README.md index 05beb7ca0..0a02bda2c 100644 --- a/python-multiplatform/src/artMain/README.md +++ b/python-multiplatform/src/artMain/README.md @@ -1,4 +1,4 @@ -# artMain — rules +# artMain, rules androidNative sources shared only by `androidNativeArm64` and `androidNativeX64`. iOS depends on `nativeMain` directly and never sees this, which is what makes it the right home for JNI code. @@ -29,5 +29,5 @@ once. Measured against the per-call equivalent on device: | `Python3.exec` | 17799.46 ns | 10123.94 ns | 1.8x | They must take and return primitives, or fill a caller-provided array: Kotlin/Native objects -cannot cross to the JVM. And they run Python, so they use ordinary JNI — a GC-blocking +cannot cross to the JVM. And they run Python, so they use ordinary JNI, a GC-blocking convention would be wrong regardless of speed. diff --git a/python-multiplatform/src/artMain/kotlin/python/multiplatform/env/PythonPayload.art.kt b/python-multiplatform/src/artMain/kotlin/python/multiplatform/env/PythonPayload.art.kt index 4067deaa0..d4bbdff14 100644 --- a/python-multiplatform/src/artMain/kotlin/python/multiplatform/env/PythonPayload.art.kt +++ b/python-multiplatform/src/artMain/kotlin/python/multiplatform/env/PythonPayload.art.kt @@ -4,7 +4,7 @@ package python.multiplatform.env * androidNative has no packaging step to receive a payload from, so there is nothing to discover. * * This target is the Kotlin/Native half of Android: it is built into a `.so` that the JVM half - * loads through JNI, and everything an Android app *ships* — assets included — is packaged by AGP + * loads through JNI, and everything an Android app *ships*, assets included, is packaged by AGP * on the JVM side. `assets/python/` is therefore read by `androidMain`'s * [PythonBootstrap.stagePayload], which has the `Context` that reading an APK's assets requires, * and this side sees the result as a plain directory that is already on `sys.path` by the time any diff --git a/python-multiplatform/src/artMain/kotlin/python/multiplatform/ffi/JniExport.kt b/python-multiplatform/src/artMain/kotlin/python/multiplatform/ffi/JniExport.kt index 413bdd7bf..877c11aa2 100644 --- a/python-multiplatform/src/artMain/kotlin/python/multiplatform/ffi/JniExport.kt +++ b/python-multiplatform/src/artMain/kotlin/python/multiplatform/ffi/JniExport.kt @@ -126,7 +126,7 @@ fun sayHello() { * Diagnostic only: returns its argument unchanged. * * Exported under `python.native.ffi.bindings` so the Android JVM side can call it through the same - * path as every other binding. Its whole purpose is to make an argument-slot mismatch visible — + * path as every other binding. Its whole purpose is to make an argument-slot mismatch visible, * if the JNI calling convention does not match this export's signature, the value read back will * not be the value passed in. */ diff --git a/python-multiplatform/src/commonMain/README.md b/python-multiplatform/src/commonMain/README.md index ac0c24332..ba88cf071 100644 --- a/python-multiplatform/src/commonMain/README.md +++ b/python-multiplatform/src/commonMain/README.md @@ -1,8 +1,8 @@ -# commonMain — rules +# commonMain, rules The object model and the `expect` declarations every platform implements. -## Layering — the object model must not reach past the FFI layer +## Layering, the object model must not reach past the FFI layer python.multiplatform.ffi PyObject, PyType, collections, conversion | may call @@ -13,7 +13,7 @@ The object model and the `expect` declarations every platform implements. python.native.ffi.bindings JNI externals / Panama handles `PyObject` and friends call EmbedAPI functions and nothing below them. `bindings` is a platform -detail that only `EmbedAPI..kt` may know about — it does not exist at all on the +detail that only `EmbedAPI..kt` may know about, it does not exist at all on the native targets, which use cinterop. A composed operation is a platform implementation difference, so it belongs in the FFI layer as @@ -34,18 +34,18 @@ requirement that a thread be attached before touching any object. records scopes *Kotlin* opened, and C is free to have dropped the GIL inside one of them: `ctypes.CFUNCTYPE` releases it around every foreign call (`PYFUNCTYPE` does not). So a callback entered from C can run with a non-zero depth and no thread state at all, and the first C API call -segfaults inside `_PyThreadState_GET` — observed here as `PyErr_Occurred+0x1c`. +segfaults inside `_PyThreadState_GET`, observed here as `PyErr_Occurred+0x1c`. Anything Python calls into therefore takes its own `PyGILState_Ensure`/`Release` pair unconditionally and restores the counters around it; `python.multiplatform.ffi.upcall .UpcallTrampoline.attached` is that. It does **not** apply to CPython's own type slots (`tp_traverse`, `tp_clear`, `tp_dealloc`), which are entered with the GIL held and must neither -take nor release it — see `ProxyTypeFactory`. +take nor release it, see `ProxyTypeFactory`. ### An asynchronous completion is a second entry point, on a thread nobody attached An exposed `suspend fun` returns an `asyncio.Future` and its value arrives later, on whatever -thread resumed the coroutine — which is generally not the thread that took the upcall, and may be +thread resumed the coroutine, which is generally not the thread that took the upcall, and may be one CPython has never seen. That thread takes its own `withGIL` scope before touching anything (`python.multiplatform.ffi.upcall.AsyncUpcall`), and `withGIL` is right here where it is wrong for a C entry point: this is *not* reached from C, so the thread's nesting depth is its own honest @@ -53,20 +53,20 @@ record. A completer thread with depth 0 takes a real `PyGILState_Ensure`; a comp from inside a later upcall correctly skips, under the depth that upcall's `attached` set. The same acquisition is what publishes `PendingCall`'s fields. That class is unsynchronised on the -stated condition that every mutation happens on a GIL-holding thread — so **a dispatcher that +stated condition that every mutation happens on a GIL-holding thread, so **a dispatcher that resumes an exposed call's continuation must hold the GIL while it does so**, not merely take it afterwards to talk to Python. Measured: `AsyncCompletionProbeTest` (a Kotlin-created thread can take the GIL while the interpreter is parked in `run_until_complete`) and `AsyncUpcallDeliveryTest`. -## Attaching is not enough — someone has to reach an eval-loop checkpoint +## Attaching is not enough, someone has to reach an eval-loop checkpoint `_Py_HandlePending` is the only place CPython merges the free-threaded build's deferred reference-count queue, processes QSBR-deferred frees, runs a *scheduled* cyclic collection and drains pending calls and signals. Nothing in the C API calls it; only the `_CHECK_PERIODIC` uop that opens every Python-level frame does. So an embedder that never executes bytecode never reaches one, and a reference the cleaner gave back on a free-threaded build stays unreclaimed -however long you wait — the count is right, the memory is not returned. +however long you wait, the count is right, the memory is not returned. `Python3.drainPendingReleases()` reaches one, by calling a cached empty Python function (measured 355 ns, against 11 µs for `exec("pass")`). It also runs automatically once every @@ -77,13 +77,13 @@ proved that deadlocks. See ROADMAP §9. ### The GIL build needs the same checkpoint, for cyclic garbage specifically -`_PY_GC_SCHEDULED_BIT` — set by `_Py_ScheduleGC` on an allocation that crosses a generation -threshold, and how allocation has triggered a cyclic collection since 3.12 — is read by the same +`_PY_GC_SCHEDULED_BIT`, set by `_Py_ScheduleGC` on an allocation that crosses a generation +threshold, and how allocation has triggered a cyclic collection since 3.12, is read by the same `_Py_HandlePending` as the free-threaded merge above, and nowhere else. That part of the problem is **not free-threading-specific**: an embedder that only calls `PyObject_Call` and friends never runs the cyclic collector on either build, because neither build ever executes a bytecode frame on -its own. A plain (non-cyclic) reference still frees immediately on the GIL build — there is no -queue there — but a reference cycle accumulates exactly as it would on the free-threaded build's +its own. A plain (non-cyclic) reference still frees immediately on the GIL build, there is no +queue there, but a reference cycle accumulates exactly as it would on the free-threaded build's deferred-release queue. Measured: 10,000 cyclic groups built purely through the C API leave 20,000+ objects uncollected indefinitely with `autoDrainInterval = 0`; the same workload with `autoDrainInterval` on leaves only what has accumulated since the last checkpoint. See @@ -92,15 +92,15 @@ deferred-release queue. Measured: 10,000 cyclic groups built purely through the `Python3.autoDrainInterval` therefore defaults to **off** on the GIL build too, and that default is deliberate rather than an oversight: turning it on is a real behavioural change, not a free one. A checkpoint tripped inside an ordinary `withGIL` scope can run whatever the collector or the -release queue was holding onto — `__del__`, weakref callbacks, pending calls — at a point the +release queue was holding onto, `__del__`, weakref callbacks, pending calls, at a point the caller never asked to yield from. Measured reentrant-safe (a `__del__` that calls back into -Kotlin mid-collection completes without deadlocking, looping or crashing — +Kotlin mid-collection completes without deadlocking, looping or crashing, `GCSchedulingMeasurementTest.testReentrancyDuringCheckpoint`), and cheap (~5.5 ns per outermost -`withGIL` scope, ~2.9% of the floor, amortised over the interval) — but still a side effect an +`withGIL` scope, ~2.9% of the floor, amortised over the interval), but still a side effect an embedder has to opt into with eyes open, by setting `autoDrainInterval` explicitly or calling `Python3.drainPendingReleases()`. An embedder that would rather force an immediate, unconditional collection without touching either can call `PyGC_Collect()` directly (`python.native.ffi`, or -`python.multiplatform.ffi.utils.PyGC.collect()` for the `gc.collect()`-equivalent route) — the +`python.multiplatform.ffi.utils.PyGC.collect()` for the `gc.collect()`-equivalent route), the same function `drainPendingReleases()` documents as the way to reclaim on behalf of *another* thread. @@ -109,9 +109,9 @@ thread. CPython's C API is inconsistent about ownership and getting it wrong is silent. Write which one applies: -- `PyTuple_SetItem`, `PyList_SetItem` **steal** — do not release afterwards -- `PyTuple_GetItem`, `PyDict_GetItem` **borrow** — do not release; incref to keep -- most `PyObject_*` return **new references** — release when done +- `PyTuple_SetItem`, `PyList_SetItem` **steal**, do not release afterwards +- `PyTuple_GetItem`, `PyDict_GetItem` **borrow**, do not release; incref to keep +- most `PyObject_*` return **new references**, release when done `RefCountTest` checks the balance in both directions. One too few leaks; one too many frees an object still in use, and that crash surfaces somewhere unrelated. @@ -122,7 +122,7 @@ object still in use, and that crash surfaces somewhere unrelated. together. The rule is per type, not per shape: cache what copied itself out of CPython (`Long`, `Double`, `Boolean`, a decoded `String`, a container recursively built out of those), refuse what would still point into Python-owned memory (`bytes`/`bytearray`/`memoryview`, whose native form is -a pointer into the object's own buffer), and never store a bare `NativePointer` — what +a pointer into the object's own buffer), and never store a bare `NativePointer`, what `ConversionStrategy.RAW` hands back is an address, not a reference. `isIndependentOfPythonMemory` in `conversion/PyProxy.kt` is that rule as code, and every store to diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/Versions.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/Versions.kt index 94bb845bf..49cbc440c 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/Versions.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/Versions.kt @@ -6,7 +6,7 @@ package python.multiplatform * * This used to be an enum whose only entry was `PYTHON_3_13_0`, validated against * [BuildConfig.pythonVersion] at class-initialisation time. That made every version bump a source - * change, and failing to make it did not produce a build error — it produced an + * change, and failing to make it did not produce a build error, it produced an * `ExceptionInInitializerError` the first time anything touched the FFI at runtime, which is how * moving the build to 3.14.7 broke the Android path while every compile target still passed. * @@ -22,7 +22,7 @@ class Versions private constructor(val versionString: String) { * Parses `major.minor[.patch][suffix]`, e.g. `3.14.7` or `3.15.0rc1`. * * Rejects anything without at least a major and minor component, since [compactVersionString] - * — which names the shared library and the stdlib directory — depends on both. + *, which names the shared library and the stdlib directory, depends on both. */ fun parse(versionString: String): Versions { val parts = versionString.split(".") diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonDevAgent.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonDevAgent.kt new file mode 100644 index 000000000..81e105f7d --- /dev/null +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonDevAgent.kt @@ -0,0 +1,73 @@ +package python.multiplatform.env + +import python.multiplatform.ffi.Python3 +import python.native.ffi.PyImport_ExecCodeModule +import python.native.ffi.Py_CompileString +import python.native.ffi.Py_DecRef +import python.multiplatform.ffi.pyErrorOrGeneric + +/** + * The dev agent (SPEC L-12, #233): a debug-only channel from a notebook or a hot reload into this + * interpreter, designed in toolchain's `docs/design/live-connection.md` (option B). + * + * [Python3.initialize] calls [startIfRequested]. It does nothing unless the launcher set + * [ENVIRONMENT_VARIABLE] (`port=;token=[;overlay=]`) **and** the app was built for it: one of + * its installed payload roots ([PythonPayload.installed]) holds [ENABLE_MARKER]. Then the agent's Python + * ([DEV_AGENT_SOURCE], embedded from `src/commonMain/python/python_multiplatform_devagent.py`) is loaded as + * the module [MODULE] and listens on 127.0.0.1 with that token, which must be at least 32 random bytes + * (`secrets.token_urlsafe(32)`). A refused or malformed request is reported on stderr and does not stop the app. + * + * The marker is the build-time switch. This library is one artifact for an app's debug and release builds + * alike, so a constant in it cannot tell them apart; the app's build can, and toolchain stages the marker + * into a debug variant's payload only. Without it, a release build opens no port even if something starts + * its process with the variable set: "a release launch never sets it" is the launcher's promise, which this + * runtime does not rely on. + */ +internal object PythonDevAgent { + const val ENVIRONMENT_VARIABLE = "PYTHON_MULTIPLATFORM_DEV_AGENT" + const val MODULE = "python_multiplatform_devagent" + const val ENABLE_MARKER = "python_multiplatform_devagent.enable" + + private const val PY_FILE_INPUT: Int = 257 + private var loaded = false + private var requested = false + + fun startIfRequested() { + if (requested || readEnvVar(ENVIRONMENT_VARIABLE).isNullOrBlank()) return + requested = true + if (enabledRoot() == null) { + println( + "WARNING: python-multiplatform dev agent not started: $ENVIRONMENT_VARIABLE is set, but no " + + "payload root holds $ENABLE_MARKER, so this is not a build made for it (a release build).", + ) + return + } + try { + load() + Python3.exec("__import__(\"$MODULE\").start_from_env()") + } catch (e: Exception) { + println("WARNING: python-multiplatform dev agent not started: ${e.message}") + } + } + + /** The installed payload root that holds [ENABLE_MARKER], if any. */ + private fun enabledRoot(): String? = + PythonPayload.installed.firstOrNull { pathIsAccessible("$it/$ENABLE_MARKER") } + + /** Loads [DEV_AGENT_SOURCE] as the module [MODULE] (once). */ + fun load() { + if (loaded) return + Python3.withPython { + val code = Py_CompileString(DEV_AGENT_SOURCE, "", PY_FILE_INPUT) + ?: throw pyErrorOrGeneric("Failed to compile the dev agent") + try { + val module = PyImport_ExecCodeModule(MODULE, code) + ?: throw pyErrorOrGeneric("Failed to load the dev agent") + Py_DecRef(module) + } finally { + Py_DecRef(code) + } + } + loaded = true + } +} diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPath.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPath.kt index fb88bc157..0925b971d 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPath.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPath.kt @@ -19,7 +19,7 @@ import python.native.ffi.Py_DecRef * * That form has to be *parsed and compiled* on every call (`Python3.exec` goes through * `PyRun_String`, measured at ~11 µs for a trivial statement in `EvalCheckpointTest`), and it - * interpolates a filesystem path into Python source — a payload directory containing a quote or a + * interpolates a filesystem path into Python source, a payload directory containing a quote or a * backslash, which every Windows path does, either breaks the statement or changes what it does. * Going through the list object directly has neither problem. * @@ -34,7 +34,7 @@ import python.native.ffi.Py_DecRef * ### Ordering * * [prepend] puts the entry at `sys.path[0]`, which is where CPython itself puts the directory of - * the script being run. That is the right position for a payload — it *is* the application's own + * the script being run. That is the right position for a payload, it *is* the application's own * code, and an application must be able to ship a module that overrides one it inherits. The cost * of that choice is that a payload module named after a standard library one shadows it, so * [append] exists for an embedder who wants the opposite and knows why. @@ -101,7 +101,7 @@ object PythonPath { true } - /** `sys.path`, a **borrowed** reference — `PySys_GetObject` does not hand ownership over. */ + /** `sys.path`, a **borrowed** reference, `PySys_GetObject` does not hand ownership over. */ private fun sysPathHoldingGIL(): NativePointer? = PySys_GetObject("path") private fun readEntriesHoldingGIL(path: NativePointer): List { diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPayload.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPayload.kt index 7018fb3d8..348584356 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPayload.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/env/PythonPayload.kt @@ -13,7 +13,7 @@ package python.multiplatform.env * `toolchain`'s `49da1d8` then delivered a payload into the desktop jar's root and the APK's assets * and recorded the same finding from the other side: nothing reads it there. * - * The only written contract is the producing side's — `pypackpack`'s `ResourceBundler` KDoc: + * The only written contract is the producing side's, `pypackpack`'s `ResourceBundler` KDoc: * * > **Payload root is `python/`.** The bundle directory holds a `python/` subdirectory intended to * > be placed on `sys.path` verbatim (staged into Android assets, an iOS resource directory, or a @@ -25,20 +25,20 @@ package python.multiplatform.env * ### Where in start-up this happens, and why it cannot be earlier * * `Python3.initialize()` calls [installStagedRoots] immediately after `Py_Initialize()` has - * returned and the main thread state has been parked, before it hands control back to its caller — + * returned and the main thread state has been parked, before it hands control back to its caller, * so before any `PyImport_ImportModule` this library can reach. * * It cannot happen earlier, and that is a property of CPython rather than a choice: **`sys.path` * does not exist until `Py_Initialize()` has built it.** `PySys_GetObject("path")` before that - * point has no interpreter to read from. The two mechanisms that *do* run before initialisation — + * point has no interpreter to read from. The two mechanisms that *do* run before initialisation, * `PYTHONHOME` (an environment variable read by `getenv(3)`) and [PythonHomeCheck] (a filesystem - * probe of that variable) — are both about the standard library, and neither has a place to put a + * probe of that variable), are both about the standard library, and neither has a place to put a * consumer directory. So the payload is strictly a post-`Py_Initialize` step, and the useful * question is only whether it happens before the caller can import anything. It does. * * The alternative considered was `PYTHONPATH`, which *is* read at initialisation and would need no * post-step. It was rejected: a JVM cannot set an environment variable for itself (see - * `desktopMain/README.md` — the same reason `PYTHONHOME` is set by the Gradle plugin as the + * `desktopMain/README.md`, the same reason `PYTHONHOME` is set by the Gradle plugin as the * process is launched), so on the platform this had to work on first it is not available at all. * * ### It stays automatic @@ -50,7 +50,7 @@ package python.multiplatform.env object PythonPayload { /** - * `python` — `ResourceBundler.PYTHON_ROOT`, the directory name its manifest records as + * `python`, `ResourceBundler.PYTHON_ROOT`, the directory name its manifest records as * `pythonRoot`, and what `toolchain`'s `PythonStagingLayout.PAYLOAD_ROOT` stages under on all * three platforms. */ @@ -59,7 +59,7 @@ object PythonPayload { /** * Whether `Python3.initialize()` puts discovered payload roots on `sys.path` for you. * - * Set it to false *before* initialising to take the job over — nothing re-reads it afterwards. + * Set it to false *before* initialising to take the job over, nothing re-reads it afterwards. */ var autoInstall: Boolean = true @@ -105,16 +105,16 @@ object PythonPayload { * The `Python3.initialize()` hook. Does nothing when [autoInstall] is off, and does its work at * most once per process. * - * The once-only flag is not an optimisation, though it is also that — discovery is filesystem + * The once-only flag is not an optimisation, though it is also that, discovery is filesystem * work on every platform and a class path scan on desktop. It is what lets * `Python3.initialize()` call this on the path where it *returns early* because the interpreter * was already up. That path is real and is exactly where a payload would otherwise be lost: * `Python3.isInitialized` is seeded from `Py_IsInitialized()`, so any component that reached - * `Py_Initialize()` first — Android's `PythonInstrumentationRunner` did precisely this, see - * `androidMain/README.md` — latches it to true and makes every later `initialize()` a no-op. + * `Py_Initialize()` first, Android's `PythonInstrumentationRunner` did precisely this, see + * `androidMain/README.md`, latches it to true and makes every later `initialize()` a no-op. * * Failures propagate rather than being swallowed. A payload that was staged but cannot be read - * is a broken build, and the alternative — starting anyway — produces a `ModuleNotFoundError` + * is a broken build, and the alternative, starting anyway, produces a `ModuleNotFoundError` * at whatever point the application first imports itself, with nothing left to say why. */ internal fun installStagedRootsOnStartup() { @@ -131,7 +131,7 @@ object PythonPayload { * can go on `sys.path` as they are. * * Returns an empty list when there is no payload, which is the normal state of an application that - * has not configured one. Implementations may have to *materialise* the directory first — a + * has not configured one. Implementations may have to *materialise* the directory first, a * classpath resource inside a jar and an entry in an APK's asset archive are both readable by the * platform and not by CPython's importer, which opens files with `open(2)`. */ diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/GILScope.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/GILScope.kt index 889e87f16..f58002b10 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/GILScope.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/GILScope.kt @@ -29,7 +29,7 @@ public class ThreadGILState(var state: Int = 0, var depth: Int = 0) { * release mark skips it entirely when no reference has been given back since the previous one, so * a workload that allocates nothing pays a field compare and nothing else. * - * Not inline, so that [withGIL] — which is — can reach the internal counters through it. + * Not inline, so that [withGIL], which is, can reach the internal counters through it. */ @PublishedApi internal fun maybeReachEvalCheckpoint(tState: ThreadGILState) { diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt index 48aa9cc63..ca637c399 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt @@ -2,6 +2,7 @@ package python.multiplatform.ffi import python.multiplatform.BuildConfig import python.multiplatform.env.PythonHomeCheck +import python.multiplatform.env.PythonDevAgent import python.multiplatform.env.PythonPayload import python.multiplatform.env.applyPackagedPythonHome import python.multiplatform.ffi.exceptions.PyException @@ -51,6 +52,7 @@ object Python3 { // does its work at most once per process, so arriving here repeatedly is free. PythonPayload.installStagedRootsOnStartup() installBuiltinCompiledOnce() + PythonDevAgent.startIfRequested() return } // Runs once per process, before the call whose own failure mode is either an uncatchable @@ -94,6 +96,9 @@ object Python3 { // is a separate mechanism entirely -- see PythonPayload for the whole reasoning, and for // the `autoInstall` switch that turns this off. PythonPayload.installStagedRootsOnStartup() + // The dev agent (SPEC L-12): only when a debug launch asked for it, after the payload is on + // sys.path so its overlay can go in front of it. + PythonDevAgent.startIfRequested() } private var builtinCompiledInstalled = false @@ -181,8 +186,8 @@ object Python3 { * ### What this is instead of * * The body this replaces called `Py_RunMain()`, and the reason that is not a bug that can be - * patched is in the header: `Py_RunMain()` is declared in `cpython/pylifecycle.h` — a - * *lifecycle* function, and not part of the Limited API this binding otherwise targets — and + * patched is in the header: `Py_RunMain()` is declared in `cpython/pylifecycle.h`, a + * *lifecycle* function, and not part of the Limited API this binding otherwise targets, and * its contract is to run whatever `PyConfig.run_command`/`run_module`/`run_filename` names * **and then finalize Python**. There is no mode in which it leaves the runtime standing. So * it destroyed the interpreter it was asked to run a module in, while [isInitialized] still @@ -199,7 +204,7 @@ object Python3 { * * ### What it does * - * `runpy.run_module(moduleName, run_name="__main__", alter_sys=True)` — the standard library's + * `runpy.run_module(moduleName, run_name="__main__", alter_sys=True)`, the standard library's * own answer, and the machinery CPython's `-m` switch itself goes through * (`runpy._run_module_as_main` shares `_get_module_details`/`_run_code` with it). It touches * no lifecycle function, so nothing here can finalize anything. Concretely, `alter_sys=True` @@ -207,7 +212,7 @@ object Python3 { * * - `_TempModule("__main__")` installs a **fresh** module as `sys.modules["__main__"]` for the * duration and puts the previous one back in `__exit__`. The module therefore gets a clean - * `__main__` namespace, and the embedder's own `__main__` — the one [exec] writes into — is + * `__main__` namespace, and the embedder's own `__main__`, the one [exec] writes into, is * neither read nor written, and is restored whether the module returns or raises. * - `_ModifiedArgv0(spec.origin)` points `sys.argv[0]` at the module's file while it runs and * restores it afterwards, which is what `python -m` does. @@ -218,7 +223,7 @@ object Python3 { * including when the module raises. Two halves to that: * * - It has to be *set*, because `runpy`'s `_ModifiedArgv0.__enter__` reads `sys.argv[0]` - * before writing it. An embedded interpreter is not guaranteed to have a usable one — this + * before writing it. An embedded interpreter is not guaranteed to have a usable one, this * is the same soft spot the old body fell into from the other side. * - It has to be *restored*, because it is process-global state that the caller did not ask * to have rewritten. A module run is a nested activity here, not the process's reason for @@ -230,7 +235,7 @@ object Python3 { * ### Exceptions, and `sys.exit()` * * An exception escaping the module body propagates as a [PyException] carrying the real - * Python type, message and traceback, and the interpreter stays usable — a failing module is + * Python type, message and traceback, and the interpreter stays usable, a failing module is * an ordinary failure, not a poisoned runtime. * * `SystemExit` is deliberately **not** in that class. A module written to be run as `__main__` @@ -330,10 +335,10 @@ object Python3 { * The Python side of [runMain]. * * It is written in Python rather than assembled out of C API calls because every line of it - * is a `try`/`finally` or an `except` — restoring `sys.argv` on both paths, and telling + * is a `try`/`finally` or an `except`, restoring `sys.argv` on both paths, and telling * `SystemExit` apart from a real failure. Expressing that through the C API would mean * `PyErr_GetRaisedException` plus a `PyErr_GivenExceptionMatches` against a `SystemExit` this - * binding exposes no `PyExc_*` handle for, then re-raising by hand — more code, and all of it + * binding exposes no `PyExc_*` handle for, then re-raising by hand, more code, and all of it * in the one path that exists to report failures. * * What it deliberately does *not* do is catch anything but `SystemExit`: any other exception @@ -376,7 +381,7 @@ def __pmp_run_module__(mod_name, argv): /** * Compiles [RUN_MODULE_SOURCE] once into a private globals dict and keeps a strong reference - * to the resulting function, exactly as [checkpointCallableHoldingGIL] does — including the + * to the resulting function, exactly as [checkpointCallableHoldingGIL] does, including the * detail that a module-level `def` needs no `__builtins__` entry of its own, because frame * setup falls back to the interpreter's builtins when the globals mapping has none. * @@ -412,55 +417,55 @@ def __pmp_run_module__(mod_name, argv): /** * Runs [args] the way `python ` would parse and dispatch them, in an interpreter that - * **survives the call** — the same property [runMain] exists for, and for the same reason: + * **survives the call**, the same property [runMain] exists for, and for the same reason: * `Py_BytesMain(argc, argv)` is CPython's CLI `main`, it reaches `Py_RunMain()` * (`cpython/pylifecycle.h`), and that function's contract is to run and then finalize. There * is no mode in which it leaves the runtime standing, so it can never be this function's - * implementation — wiring up its `(int argc, char **argv)` marshalling would only have bought + * implementation, wiring up its `(int argc, char **argv)` marshalling would only have bought * a function with exactly the defect [runMain] was fixed to avoid. (ROADMAP §12 has the full * history: the marshalling used to be recorded as the blocker, but it was never the real one.) * * ### The decided, deliberately smaller scope * - * `Py_BytesMain` also *parses* the command line — `-c`, `-m`, a script path, `-`, and a long - * tail of single-letter flags — and reimplementing all of that against a `PyConfig` this + * `Py_BytesMain` also *parses* the command line, `-c`, `-m`, a script path, `-`, and a long + * tail of single-letter flags, and reimplementing all of that against a `PyConfig` this * binding does not carry (struct layout is exactly what the Stable ABI does not promise) is * not worth paying for options that mostly cannot mean anything here anyway. So this parses a * deliberately reduced subset of the same grammar: * - * - **`-c `** — runs `` as a statement sequence in the embedder's own, persistent - * `__main__` — the same namespace [exec] writes into. `sys.argv[0]` is `"-c"`, matching + * - **`-c `**, runs `` as a statement sequence in the embedder's own, persistent + * `__main__`, the same namespace [exec] writes into. `sys.argv[0]` is `"-c"`, matching * CPython (`python3 -c "import sys;print(sys.argv)" a b` → `['-c', 'a', 'b']`, verified * against the system interpreter while designing this). - * - **`-m `** — a straight delegation to [runMain], which already *is* `python -m`'s + * - **`-m `**, a straight delegation to [runMain], which already *is* `python -m`'s * implementation (`runpy.run_module(..., alter_sys=True)`). Nothing here duplicates it. - * - **a bare script path** — reads the file (through Python's own `open()`, not a new - * per-platform Kotlin file API — `nativeMain` has no filesystem abstraction of its own, and + * - **a bare script path**, reads the file (through Python's own `open()`, not a new + * per-platform Kotlin file API, `nativeMain` has no filesystem abstraction of its own, and * CPython already has one that works identically everywhere this binding runs) and runs it, * again in the embedder's persistent `__main__`. `sys.argv[0]` is the path exactly as - * given — CPython does not resolve it (`cd /tmp && python3 argvtest.py` → `sys.argv[0] == + * given, CPython does not resolve it (`cd /tmp && python3 argvtest.py` → `sys.argv[0] == * 'argvtest.py'`, not an absolute path; verified the same way). - * - **`--` followed by a path** — forces the next token to be read as a script path even if it + * - **`--` followed by a path**, forces the next token to be read as a script path even if it * looks like an option, mirroring CPython's own observed behaviour: `python3 -- -c args` * does *not* run `-c` as a command, it tries to open a file literally named `-c`. This binding * does not carry `-c`/`-m` support past a leading `--`, since CPython's own parser does not * either once option scanning has been told to stop. * - **`-`, and anything else starting with `-`** (`-E -I -S -s -B -O -OO -X -W -u -P` among - * them, per the design that fixed this scope) — refused **by name** rather than silently + * them, per the design that fixed this scope), refused **by name** rather than silently * ignored. `-` means "read the program from stdin", which an embedded interpreter has no * sane default for. The rest are `PyConfig` fields (`isolated`, `use_environment`, * `site_import`, `user_site_directory`, `write_bytecode`, `optimization_level`, `Xoptions`, * warning filters, unbuffered I/O, `safe_path`) that only mean anything **before** - * `Py_Initialize()` — this interpreter is already initialized by the time [runApp] is + * `Py_Initialize()`, this interpreter is already initialized by the time [runApp] is * called, so honouring them would mean either lying about what happened or silently doing * nothing, and the second one is exactly the defect this function used to have. Naming the * option in the refusal is deliberate: a caller who typed `-I` expecting isolation and got a * generic "unsupported" would not necessarily notice their isolation request was dropped. - * - **empty `args`** — refused the same way, since CPython's own default with nothing on the + * - **empty `args`**, refused the same way, since CPython's own default with nothing on the * command line is also to read from stdin (interactively, if it is a tty). * * Anything after the mode-selecting token (the command, the module name, or the script path) - * is passed through verbatim as `sys.argv[1:]` — no re-parsing, so an argument to the + * is passed through verbatim as `sys.argv[1:]`, no re-parsing, so an argument to the * caller's own script that happens to start with `-` is never mistaken for one of the options * above. * @@ -475,7 +480,7 @@ def __pmp_run_module__(mod_name, argv): * returning and the raising path. * * @param args the command-line arguments *after* the program name, e.g. `["-c", "print(1)"]` - * or `["script.py", "--flag"]` — the same slice `Py_BytesMain(argc, argv)` would see as + * or `["script.py", "--flag"]`, the same slice `Py_BytesMain(argc, argv)` would see as * `argv[1:]`. * @return the exit status: 0 for a clean run, otherwise whatever the program passed to * `sys.exit()`. @@ -540,10 +545,10 @@ def __pmp_run_module__(mod_name, argv): * a `SystemExit` handle this binding exposes no accessor for. * * `__pmp_run_file__` reads the script through Python's own `open()` rather than a Kotlin file - * API deliberately — `nativeMain` has no cross-platform filesystem abstraction of its own + * API deliberately, `nativeMain` has no cross-platform filesystem abstraction of its own * (`iosMain`/`artMain` diverge below it), and CPython already has one that behaves identically * on every target this binding runs on. `compile()` is given the raw `bytes` `open("rb")` - * returns, not a decoded `str`, so a PEP 263 encoding cookie in the script is still honoured — + * returns, not a decoded `str`, so a PEP 263 encoding cookie in the script is still honoured, * passing a `str` would have silently ignored one. */ private const val RUN_APP_SOURCE = """ @@ -661,7 +666,7 @@ def __pmp_run_file__(path, argv): /** * Calls [helper] with [leadingArgs] (converted to `str`) followed by [argv] (converted to a - * `list[str]`), and reads back the `int` it returns the same way [runMain] does — [helper]'s + * `list[str]`), and reads back the `int` it returns the same way [runMain] does, [helper]'s * only non-`SystemExit` failure mode is an escaped exception, which arrives here as * `PyObject_CallObject` returning `null` with the indicator set. * @@ -735,8 +740,8 @@ def __pmp_run_file__(path, argv): /** * CPython defers a set of housekeeping jobs to a *checkpoint* that only the evaluation loop - * reaches. `_Py_HandlePending` — called from the `_CHECK_PERIODIC` uop that begins every - * Python-level frame and closes every call instruction — is the single place that + * reaches. `_Py_HandlePending`, called from the `_CHECK_PERIODIC` uop that begins every + * Python-level frame and closes every call instruction, is the single place that * * - merges the free-threaded build's biased reference-counting queue * (`_PY_EVAL_EXPLICIT_MERGE_BIT` → `_Py_brc_merge_refcounts`), @@ -759,24 +764,24 @@ def __pmp_run_file__(path, argv): * reason this is not done from the cleaner: the cleaner owns nothing, so a checkpoint taken * there would drain an empty queue while running Python on a thread whose only job is to hand * references back. Use [PyGC_Collect] instead if you need to reclaim on behalf of some other - * thread — it stops the world and merges every thread's queue, at a cost proportional to the + * thread, it stops the world and merges every thread's queue, at a cost proportional to the * heap rather than to the queue. * - * **This is not only a free-threading fix.** `_PY_GC_SCHEDULED_BIT` — the bit `_Py_ScheduleGC` - * sets on every allocation that crosses a generation threshold — is cleared by the same + * **This is not only a free-threading fix.** `_PY_GC_SCHEDULED_BIT`, the bit `_Py_ScheduleGC` + * sets on every allocation that crosses a generation threshold, is cleared by the same * `_Py_HandlePending` this reaches. That scheduling has been how allocation triggers a cyclic * collection since 3.12, on *both* builds. An embedder driving CPython purely through the C * API therefore never runs the cyclic collector at all, GIL or not: reference cycles the * collector would otherwise break simply accumulate. Calling this reclaims them exactly as it - * merges the free-threaded queue — one call, one checkpoint, both jobs. See + * merges the free-threaded queue, one call, one checkpoint, both jobs. See * `docs/investigations/gc-scheduling-investigation.md` §1 and §6. * * The trade-off is what runs at the checkpoint, not just what it reclaims: any `__del__`, * weakref callback or pending call attached to something the collector or the queue was - * holding can execute here, inside whatever `withGIL` scope happened to trip the checkpoint — + * holding can execute here, inside whatever `withGIL` scope happened to trip the checkpoint, * a scope its caller did not ask to yield control from. Measured reentrant-safe (a `__del__` * that calls back into Kotlin mid-collection completes without deadlocking, looping or - * crashing — `GCSchedulingMeasurementTest.testReentrancyDuringCheckpoint`), but it is still a + * crashing, `GCSchedulingMeasurementTest.testReentrancyDuringCheckpoint`), but it is still a * side effect the caller did not request, and is why [autoDrainInterval] defaults to off on * the GIL build rather than always on. * @@ -803,7 +808,7 @@ def __pmp_run_file__(path, argv): * released since the previous one, so a workload that never drops a wrapper never pays. * * Defaults to on for free-threaded builds, where deferred release is the defect described on - * [drainPendingReleases] — a build with the global lock frees a *plain* reference the + * [drainPendingReleases], a build with the global lock frees a *plain* reference the * cleaner's `Py_DecRef` immediately, with no queue to drain, so that half of the checkpoint's * job really is pure overhead there. * @@ -817,7 +822,7 @@ def __pmp_run_file__(path, argv): * a `withGIL` scope exit the caller never asked to yield from. Measured cost when on: ~5.5 ns * per outermost scope (~2.9% of the ~190 ns floor), amortised over this interval. Turn it on * explicitly, call [drainPendingReleases] to force one checkpoint without changing this, or - * call [PyGC_Collect] directly for an immediate, unconditional collection — [drainPendingReleases] + * call [PyGC_Collect] directly for an immediate, unconditional collection, [drainPendingReleases] * and `PyGC_Collect` both work regardless of this setting. */ var autoDrainInterval: Int = if (BuildConfig.pythonFreeThreaded) DEFAULT_AUTO_DRAIN_INTERVAL else 0 @@ -881,7 +886,7 @@ def __pmp_run_file__(path, argv): internal fun reachEvalCheckpointHoldingGIL() { // Do not disturb an error indicator its owner has not read yet. Several call sites read // the indicator *after* their `withPython { ... }` scope has closed (see - // `PyObject.getAttr`), and entering the eval loop can replace it — a pending signal + // `PyObject.getAttr`), and entering the eval loop can replace it, a pending signal // handler or a finaliser raising is enough. if (PyErr_Occurred() != null) { CheckpointCounter.skipped++ @@ -894,7 +899,7 @@ def __pmp_run_file__(path, argv): val result = PyObject_CallNoArgs(callable) if (result == null) { // The body is `pass`, so nothing raised here belongs to the caller: it came out of - // the checkpoint itself — a pending signal, a pending call, a finaliser. There is no + // the checkpoint itself, a pending signal, a pending call, a finaliser. There is no // caller to hand it to, and leaving it set would surface as a spurious failure in // whatever ran next, so it is cleared and counted. PyErr_Clear() diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonCallables.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonCallables.kt index 5304789c4..c1e85ea97 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonCallables.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonCallables.kt @@ -64,7 +64,7 @@ import python.multiplatform.reflection.TypeTag * * So this file owns the first three quarters of that -- creating, holding and releasing -- and the * one Compose type involved, `RememberObserver`, stays in the module that has Compose - * (`:ksp-fixtures:compose`'s `PythonComposition`). `docs/archive/pythonx-adapter-design.md` §6 item 1 names + * (`:python-multiplatform-compose:fixtures:compose`'s `PythonComposition`). `docs/archive/pythonx-adapter-design.md` §6 item 1 names * `onForgotten` as "the one that fails silently and should be tested first"; the seam is here and the * test is there. * @@ -106,7 +106,7 @@ import python.multiplatform.reflection.TypeTag * else. Interning bounds the case that actually recurs; the sweep needed that measurement first. * * **That measurement now exists, and the verdict is that a same-pass sweep is unsafe.** - * `:ksp-fixtures:compose`'s `RetainedSlotSweepPreconditionTest + * `:python-multiplatform-compose:fixtures:compose`'s `RetainedSlotSweepPreconditionTest * .aRetainingSlotKeepsPassZerosWrapperWhileLaterPassesCrossAndGoUntouched` drives a real * `remember(key)` (the mechanism `LaunchedEffect(key)` is built on) with a constant key across six * recompositions of one composition, each supplying a callable with a distinct capture -- so each diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapter.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapter.kt index 4fa114245..f512dd1ca 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapter.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapter.kt @@ -9,13 +9,14 @@ import python.multiplatform.reflection.UpcallTable * The binding layer -- `python_multiplatform.binding`, the hand-written Python that serves Kotlin * declarations on their Kotlin-named modules, and the generated table it reads. * - * **It renames nothing, and it is not `pythonx`.** A Kotlin-named module carries Kotlin's own - * surface: Kotlin declaration names, keyword arguments by Kotlin parameter names, Kotlin defaults, - * overloads, value-class rules and the `$composer` slot (`docs/INTENT.md` §2.2). The Kotlin - * package and class names below (`ffi.pythonx`, `PythonxAdapter`) are historical; the Python module - * this installs is binder-owned, and `pythonx` is a real package in pythonx-compose that builds a - * Pythonic API on top of this one through `python_multiplatform.describe` and `inspect.signature` - * ([KotlinSurface]). + * **It renames no namespace, and it is not `pythonx`.** A Kotlin-named module is served under its + * Kotlin package name only (`docs/INTENT.md` §2.2), with Kotlin defaults, overloads, value-class + * rules and the `$composer` slot. Inside it, every declaration, proxy member and keyword parameter + * answers to its Kotlin name **and** to its Pythonic (snake_case) alias where that alias is + * unambiguous (issue #131; the rule and the policy are [KotlinSurface]'s). The Kotlin package and + * class names below (`ffi.pythonx`, `PythonxAdapter`) are historical; the Python module this + * installs is binder-owned, and `pythonx` is a real package in pythonx-compose that builds on this + * one through `python_multiplatform.describe` and `inspect.signature` ([KotlinSurface]). * * `docs/archive/pythonx-adapter-design.md` §7 draws one line through this whole area: **if it differs per * Kotlin declaration it is generated or resolved at run time; if it is the same rule for every @@ -90,7 +91,7 @@ object PythonxAdapter { * |---|---| * | `_Finder` / `_Loader` | §2.3, the hook a module `__getattr__` cannot replace | * | the module `__getattr__` the loader installs | §4.1, adapted once and then a dict hit | - * | Kotlin names, unchanged | `docs/INTENT.md` §2.2 -- no rule converts a name in either direction | + * | `_package_aliases` / `_member_aliases` | issue #131 -- Kotlin names as keys, Pythonic aliases resolved on a miss; no namespace converted | * | `_Overloads` | `docs/design/kotlin-extensions-in-python.md` §3.1, the dispatcher that has to live here | * | `_proxy_type` / `_Hybrid` | §4.2, an extension as a method on its receiver, both spellings | * | `_coerce`'s allowlist | §4.4, `Dp` yes, a packed value class no | @@ -99,11 +100,11 @@ object PythonxAdapter { # GENERATED-FREE: this file is hand-written Python. See PythonxAdapter.kt. # # `python_multiplatform.binding` -- the binding layer over the Kotlin declarations the upcall - # table exposes. It serves Kotlin-named modules with **Kotlin's own surface**: Kotlin - # declaration names, keyword arguments by Kotlin parameter names, Kotlin defaults, overloads, - # value-class rules and the `${'$'}composer` slot. It renames nothing. Making that surface - # Pythonic is the job of a real Python package built on top of it (pythonx-compose), which - # reads `python_multiplatform.describe` and `inspect.signature` to do it by rule. + # table exposes. It serves Kotlin-named modules: Kotlin declaration names and their Pythonic + # aliases, keyword arguments by Kotlin parameter name or Pythonic alias, Kotlin defaults, + # overloads, value-class rules and the `${'$'}composer` slot. It renames no namespace: a + # Kotlin package is a module under exactly its Kotlin name. A Python package built on top of + # it (pythonx-compose) reads `python_multiplatform.describe` and `inspect.signature`. # # `docs/archive/pythonx-adapter-design.md` §7 draws the line this file lives on: *if it differs per Kotlin # declaration it is generated or resolved at run time; if it is the same rule for every declaration @@ -122,6 +123,10 @@ object PythonxAdapter { import types as _types from python_multiplatform import KOTLIN_DEFAULT, signature_of as _signature_of + # The naming rule (issue #131) lives in the root module, once, so that both installers and the + # stub generator's mirror of it agree; see `KotlinSurface`. + from python_multiplatform import keyword_slots as _keyword_slots + from python_multiplatform import pythonic_aliases as _pythonic_aliases @@ -165,6 +170,27 @@ object PythonxAdapter { # --------------------------------------------------------------------------- the table + def _as_receiver_slot(row): + '''A `METHOD` row (issue #215) restated the way an extension row reads: the receiver as slot 0. + + The boundary sends both the same tuple, `(receiver, *arguments)`: a `METHOD` keeps its receiver + out of `arity` and the slot lists (`CallableKind.METHOD.hasReceiver`) and an extension counts + it, but `pm_invoke` is handed the receiver first either way. Restating it once, here, lets a + member be bound, dispatched among its overloads, called with keywords and described exactly as + an extension on its receiver is, by the code that already does that. + ''' + (kotlin_name, arity, kind, is_suspend, names, tags, type_names, return_tag, return_type_name, + _is_extension, receiver, has_default) = row + return ( + kotlin_name, arity + 1, kind, is_suspend, + ('',) + tuple(names or ()), + ('OBJECT',) + tuple(tags or ()), + (receiver,) + tuple(type_names or ()), + return_tag, return_type_name, True, receiver, + (False,) + tuple(has_default or ()), + ) + + class _Decl: '''One row of what `PythonxAdapter.renderTable` sent, plus its resolved handle.''' @@ -173,7 +199,7 @@ object PythonxAdapter { 'param_names', 'param_tags', 'param_type_names', 'return_tag', 'return_type_name', 'is_extension', 'receiver_type_name', 'param_has_default', 'handle', 'composer_index', 'changed_slots', 'default_slots', 'return_supertypes', 'row', - 'binding', + 'binding', 'keywords', ) def __init__(self, row): @@ -187,6 +213,12 @@ object PythonxAdapter { # `inspect.signature` read, the same tuple the proxy layer publishes with. self.row = row self.binding = None + # `{keyword: slot}`, Kotlin names and Pythonic aliases (`keyword_slots`), built on the + # first call that passes a keyword rather than for every row at install. + self.keywords = None + if row[2] == 'METHOD' and row[10]: + row = _as_receiver_slot(row) + self.row = row (self.kotlin_name, self.arity, self.kind, self.is_suspend, self.param_names, self.param_tags, self.param_type_names, self.return_tag, self.return_type_name, self.is_extension, self.receiver_type_name, self.param_has_default) = row @@ -251,6 +283,13 @@ object PythonxAdapter { self.handle = _resolve(self.kotlin_name) return self.handle + def keyword_slot(self, keyword): + '''The slot [keyword] names -- a Kotlin parameter name or its Pythonic alias -- or -1.''' + keywords = self.keywords + if keywords is None: + keywords = self.keywords = _keyword_slots(self.row) + return keywords.get(keyword, -1) + _TABLE = {} # kotlin fqn -> _Decl _MODULES = [] # the Kotlin-named modules this layer has adapted @@ -260,6 +299,7 @@ object PythonxAdapter { _CHILDREN = {} # kotlin package -> the Kotlin names of its direct child packages/objects _SUPERTYPES = {} # kotlin type name -> the types it is a, nearest first _PROPERTIES = {} # kotlin type a property is read on -> {kotlin name -> _Property} + _METHODS = {} # kotlin type a member function is declared on -> {kotlin name -> [_Decl]} # The two row kinds that are attributes of a *value* rather than names in a package (issue #38): @@ -324,12 +364,14 @@ object PythonxAdapter { _invalidate() _BY_PACKAGE.clear() _BY_RECEIVER.clear() + _ALIASES.clear() _MEMBER_ALIASES.clear() _MEMBER_KEYWORDS.clear() _PACKAGES_SEEN.clear() _CHILDREN.clear() _SUPERTYPES.clear() _PROPERTIES.clear() + _METHODS.clear() present = set() for row in rows: kotlin_name = row[0] @@ -360,6 +402,13 @@ object PythonxAdapter { else: prop.setter = decl continue + if decl.kind == 'METHOD': + # A member function (issue #215) is a method of its owner's proxy and nothing else: + # `Owner.name` is not a package, so neither the package index nor the packages seen + # may learn `Owner` from it. A row with no receiver has nowhere to be served. + if decl.receiver_type_name: + _index(_METHODS.setdefault(decl.receiver_type_name, {}), decl) + continue _index(_BY_PACKAGE.setdefault(decl.package, {}), decl) if decl.is_extension and decl.receiver_type_name: _index(_BY_RECEIVER.setdefault(decl.receiver_type_name, {}), decl) @@ -436,7 +485,11 @@ object PythonxAdapter { `STATIC_GETTER` read runs Kotlin, so a description has to come from the table and never from the module attribute. An overload set's base name answers every member. ''' - decls = _BY_PACKAGE.get(kotlin_package, {}).get(name) + by_name = _BY_PACKAGE.get(kotlin_package, {}) + decls = by_name.get(name) + if not decls: + kotlin = _package_aliases(kotlin_package).get(name) + decls = by_name.get(kotlin) if kotlin is not None else None if not decls: return None return tuple(decl.row for decl in decls) @@ -444,11 +497,47 @@ object PythonxAdapter { # --------------------------------------------------------------------------- names - # **Kotlin names, unchanged, everywhere.** A Kotlin declaration is reached under its own name - # and takes keyword arguments by its own parameter names. There used to be a forward rule - # here (`fillMaxWidth` -> `fill_max_width`) and its inverse; both are gone, because a binder - # that renames is deciding what a Python API is called, and that is the business of the - # Python package built on top (`docs/INTENT.md` §2.2, §2.3). + # **Kotlin names, and their Pythonic aliases** (issue #131). A Kotlin declaration is reached + # under its own name; a lower-case-first one also under its snake_case name, and each keyword + # parameter under its Kotlin name and its snake_case one. The rule and the ambiguity policy are + # the root module's (`python_multiplatform.python_name`, `pythonic_aliases`; see + # `KotlinSurface`). Every index here stays keyed by the **Kotlin** name: an alias is resolved + # to its Kotlin name on a miss and never becomes a second key, so one declaration has one + # owner, one `_Binding`, one `_Hybrid`. + # + # What is *not* converted: a namespace. `androidx.compose.foundation.layout` is importable as + # exactly that, and no segment of a package path ever gets an alias (`docs/INTENT.md` §2.2). + + _ALIASES = {} # ('package' | 'type', kotlin name) -> {alias: kotlin name}; cleared per table + + + def _package_aliases(kotlin_package): + '''`{alias: kotlin_name}` for the bound names of one Kotlin package. + + The namespace is every bound name there -- base names, explicit `name__Types` keys, + constants -- plus its child packages and objects, which occupy a name without ever being + given an alias. Built once per package per table. + ''' + key = ('package', kotlin_package) + found = _ALIASES.get(key) + if found is None: + names = set(_BY_PACKAGE.get(kotlin_package, {})) + names.update(_CHILDREN.get(kotlin_package, ())) + found = _ALIASES[key] = _pythonic_aliases(names) + return found + + + def _member_aliases(type_name): + '''`{alias: kotlin_name}` for every member a value of [type_name] is served under. + + The namespace is the whole lookup `_member` walks -- the type and every type it is a -- so + an alias that two members of the chain would both claim is not served at all. + ''' + key = ('type', type_name) + found = _ALIASES.get(key) + if found is None: + found = _ALIASES[key] = _pythonic_aliases(_member_names(type_name)) + return found _KOTLIN_PRIMITIVES = frozenset(( 'kotlin.Byte', 'kotlin.Short', 'kotlin.Int', 'kotlin.Long', 'kotlin.Float', 'kotlin.Double', @@ -500,6 +589,8 @@ object PythonxAdapter { def _simple_name(qualified): if not qualified: return '?' + # A functional-interface slot (issue #216) is named by its interface in every message. + qualified = qualified.partition(_SAM_SEPARATOR)[0] # A function-typed slot's name is a signature rather than a classifier, so its last # dotted component is the *return* type: `kotlin.Function3@Composable(…)->kotlin.Unit` # would print as `Unit`, which names the one part of it a reader does not need. Rendered @@ -572,6 +663,10 @@ object PythonxAdapter { # ------------------------------------------------------------------- callables (§6) _FUNCTION_PREFIX = 'kotlin.Function' + # `Interface|kotlin.FunctionN(...)->kotlin.Unit`: a functional-interface slot (issue #216), filled + # from a Python callable as that function type or from a handle checked against the interface. + # Must match `ArtifactScanner.SAM_SEPARATOR`. + _SAM_SEPARATOR = '|' # What `ArtifactScanner.functionSlotTypeName` appends to a slot whose declared # `kotlin.FunctionN` was lowered by the Compose plugin into a JVM `FunctionN+2`. Not a legal @@ -971,10 +1066,11 @@ object PythonxAdapter { name only when a call to it passes keyword arguments, and then only for its keyword map (the answer must name that same member). `keyword_map` is `{python_kw: kotlinParam}`, applied to that member's calls; keywords it does not name pass through unchanged. - The binder renames nothing by itself: with no resolver an unknown name is an `AttributeError` - and a keyword is a Kotlin parameter name. Answers are cached in this registry, never - written onto the proxy class, so `dir()` of a proxy shows Kotlin names only. Registering - the same `fn` twice is a no-op. + Resolvers are asked before the binder's own Pythonic alias (issue #131): with no resolver + answering, `fill_max_width` is still served as `fillMaxWidth`, and a keyword is a Kotlin + parameter name or its alias. Answers are cached in this registry, never written onto the + proxy class, so `dir()` of a proxy shows Kotlin names only. Registering the same `fn` + twice is a no-op. ''' if not callable(fn): raise TypeError('a member resolver must be callable') @@ -1129,9 +1225,12 @@ object PythonxAdapter { if not _attach(cls, name): resolved = _resolve_member(cls, name) if resolved is None: - raise AttributeError( - 'no Kotlin extension named ' + name + ' on ' + cls._kotlin_type_name - ) + kotlin = _member_aliases(cls._kotlin_type_name).get(name) + if kotlin is None: + raise AttributeError( + 'no Kotlin extension named ' + name + ' on ' + cls._kotlin_type_name + ) + return getattr(cls, kotlin) return _aliased(getattr(cls, resolved[0]), resolved[1]) return getattr(cls, name) @@ -1158,7 +1257,9 @@ object PythonxAdapter { prop = _PROPERTIES.get(candidate, {}).get(name) if prop is not None: return prop - decls = _BY_RECEIVER.get(candidate, {}).get(name) + # A member function before an extension of the same name on the same type: Kotlin + # resolves the member (issue #215). + decls = _METHODS.get(candidate, {}).get(name) or _BY_RECEIVER.get(candidate, {}).get(name) if decls: return decls return None @@ -1169,7 +1270,7 @@ object PythonxAdapter { names = [] seen = set() for candidate in _served_types(type_name): - for table in (_PROPERTIES, _BY_RECEIVER): + for table in (_PROPERTIES, _METHODS, _BY_RECEIVER): for name in table.get(candidate, {}): if name not in seen: seen.add(name) @@ -1186,6 +1287,9 @@ object PythonxAdapter { declaration its name stands for, an overload set's base name included. ''' found = _member(type_name, name) + if found is None: + kotlin = _member_aliases(type_name).get(name) + found = _member(type_name, kotlin) if kotlin is not None else None if found is None: return None if isinstance(found, _Property): @@ -1288,9 +1392,13 @@ object PythonxAdapter { if not _attach(type(self), name): resolved = _resolve_member(type(self), name) if resolved is None: - raise AttributeError( - 'no Kotlin extension named ' + name + ' on ' + type(self)._kotlin_type_name - ) + # The binder's own alias, last: a resolver may say otherwise (`KotlinSurface`). + kotlin = _member_aliases(type(self)._kotlin_type_name).get(name) + if kotlin is None: + raise AttributeError( + 'no Kotlin extension named ' + name + ' on ' + type(self)._kotlin_type_name + ) + return getattr(self, kotlin) return _aliased(getattr(self, resolved[0]), resolved[1]) return getattr(self, name) @@ -1301,11 +1409,19 @@ object PythonxAdapter { # from that dict instead of Kotlin (issue #38: `state.value = 7` was "accepted"). # So a write attaches first; `object.__setattr__` then goes through the `property` # (its setter, or Python's own refusal for a `val`). A name no Kotlin member has is - # stored on the instance as before. + # stored on the instance as before -- unless it is the Pythonic alias of one + # (`selected_index = 2` for `selectedIndex`, issue #131), which writes that member. if not name.startswith('_'): cls = type(self) - if name not in cls.__dict__: - _attach(cls, name) + if name not in cls.__dict__ and not _attach(cls, name): + kotlin = _member_aliases(cls._kotlin_type_name).get(name) + if kotlin is not None: + if kotlin not in cls.__dict__: + _attach(cls, kotlin) + # Only a property is written through; an alias of a function is not a + # place to store anything, so it is left to the instance as before. + if isinstance(cls.__dict__.get(kotlin), property): + name = kotlin _set(self, name, value) def __repr__(self): @@ -1394,6 +1510,43 @@ object PythonxAdapter { _LONG_MIN = -(1 << 63) _LONG_MAX = (1 << 63) - 1 + # Issue #146: the range of each Kotlin integral type an INT slot can declare. A slot whose + # declared type is not listed (a value class over `Int`, say) is not range-checked here. + _INTEGRAL_RANGES = { + 'kotlin.Byte': (-(1 << 7), (1 << 7) - 1), + 'kotlin.Short': (-(1 << 15), (1 << 15) - 1), + 'kotlin.Int': (_INT_MIN, _INT_MAX), + 'kotlin.Long': (_LONG_MIN, _LONG_MAX), + } + + + def _literal_misfits(decl, args, kwargs): + '''How many int arguments land in an integral slot of another width than their Kotlin literal. + + Kotlin types an integer literal as `Int` when it fits in 32 bits and as `Long` otherwise, + and resolves overloads on that type: `Color(0x11223344)` is `Color(Int)`, + `Color(0xFFFFFFFF)` is `Color(Long)`. `_Overloads` prefers the candidates with the fewest + misfits among those still tied, so a call picks the overload Kotlin would (#146). + ''' + if decl.composer_index >= 0 or not decl.param_type_names: + return 0 + pairs = list(enumerate(args)) + for key, value in kwargs.items(): + index = decl.keyword_slot(key) + if index >= 0: + pairs.append((index, value)) + misfits = 0 + for index, value in pairs: + if index >= len(decl.param_type_names) or isinstance(value, bool) or not isinstance(value, int): + continue + declared = decl.param_type_names[index] + if declared not in _INTEGRAL_RANGES: + continue + literal = 'kotlin.Int' if _INT_MIN <= value <= _INT_MAX else 'kotlin.Long' + if declared != literal: + misfits += 1 + return misfits + # What `_coerce` puts in a scalar's slot during a *trial* bind, for `_CALLABLE_PENDING`'s # reason: boxing crosses into Kotlin and roots the box, which a candidate that then loses # has no business doing. The winner is re-bound strictly before the call. @@ -1456,6 +1609,11 @@ object PythonxAdapter { def _coerce(value, tag, type_name, decl, slot, strict): '''One argument on its way in. Returns the marshalled value, or raises when `strict`.''' + # A functional-interface slot (issue #216): the interface is what a handle is checked against, + # the function type what a callable is made into. + callable_as = None + if type_name and _SAM_SEPARATOR in type_name: + type_name, _, callable_as = type_name.partition(_SAM_SEPARATOR) if value is None: # `None` written for a slot that has no default to fall back on: Kotlin's `null`, which # a nullable declaration takes (`Icon(..., contentDescription=None)`) and a non-null @@ -1497,7 +1655,7 @@ object PythonxAdapter { # made itself. Everything else in this branch requires a handle, because a Kotlin # object is the only thing a Kotlin parameter can hold; a function is the exception # because Kotlin can be given one that calls back. - slot = _function_slot(type_name) + slot = _function_slot(callable_as or type_name) if slot is not None and _is_python_callable(value): # A Python callable cannot stand in for a lambda that has to give something back. # `kotlin.Function1` is the spelling of both `(Float) -> Unit` and @@ -1516,7 +1674,8 @@ object PythonxAdapter { return _CALLABLE_PENDING return _make_function(value, slot) if slot is not None and getattr(value, '_pm_handle', None) is None: - # A function slot and something that is not callable. Refused with the signature + # A function slot and something that is not callable (or, for a functional + # interface, not a handle either). Refused with the signature # rather than falling through to "expected a handle": Kotlin will never hand a # `FunctionN` back out for Python to pass in again, so a handle is not the answer # here and saying so would send the caller looking for one. @@ -1564,6 +1723,16 @@ object PythonxAdapter { ': it is a value class and not on the raw-primitive allowlist, so ' + repr(value) + ' would be reinterpreted rather than converted', ) + if tag == 'INT' and isinstance(value, int): + # Issue #146: an integral slot takes an int only inside its Kotlin type's range, + # as `kotlinc` would refuse the literal. Without this an `Int` slot also matched + # `0xFFFFFFFF`, and `Color(0xFFFFFFFF)` tied with `Color(Long)`. + bounds = _INTEGRAL_RANGES.get(type_name) + if bounds is not None and not (bounds[0] <= value <= bounds[1]): + return _refuse( + strict, + repr(value) + ' does not fit in ' + _simple_name(type_name), + ) return float(value) if tag == 'FLOAT' else int(value) if tag == 'BOOLEAN': if not isinstance(value, bool): @@ -1706,15 +1875,12 @@ object PythonxAdapter { bit_offset = 1 if decl.is_extension else 0 if kwargs: for key, value in kwargs.items(): - index = -1 - for slot in range(declared): - # `` is deliberately not a Python identifier, so no keyword can name - # it. Every other slot answers to its Kotlin parameter name, exactly. - if decl.param_names[slot] == '': - continue - if decl.param_names[slot] == key: - index = slot - break + # `` is deliberately not a Python identifier, so no keyword can name it, + # and the synthetic slots after `${'$'}composer` answer to none. Every other slot + # answers to its Kotlin parameter name and to its Pythonic alias (`keyword_slots`). + index = decl.keyword_slot(key) + if index >= declared: + index = -1 if index < 0: return _refuse(strict, 'has no parameter named ' + key) if slots[index] is not _NO_MATCH: @@ -1769,11 +1935,8 @@ object PythonxAdapter { if not decl.param_names: return _refuse(strict, 'the producer supplied no parameter names, so it is positional only') for key, value in kwargs.items(): - index = -1 - for slot, name in enumerate(decl.param_names): - if name != '' and name == key: - index = slot - break + # A Kotlin parameter name or its Pythonic alias; never the receiver. + index = decl.keyword_slot(key) if index < 0: return _refuse(strict, 'has no parameter named ' + key) if slots[index] is not _NO_MATCH: @@ -1890,6 +2053,12 @@ object PythonxAdapter { if matched: fewest = min(entry[2] for entry in matched) matched = [entry for entry in matched if entry[2] == fewest] + if len(matched) > 1: + # Still tied: prefer the overload whose integral slots match each int's Kotlin + # literal type (#146). Candidates that agree on that stay tied and are refused. + scored = [(_literal_misfits(entry[0], args, kwargs), entry) for entry in matched] + least = min(score for score, _ in scored) + matched = [entry for score, entry in scored if score == least] if len(matched) == 1: decl = matched[0][0] # Re-bound strictly, and the trial binding above is discarded. A trial bind is a @@ -2031,6 +2200,18 @@ object PythonxAdapter { raise AttributeError(name) adapted, cacheable = _adapt(kotlin_package, name) if adapted is None: + kotlin = _package_aliases(kotlin_package).get(name) + if kotlin is not None: + # A Pythonic alias (issue #131): whatever the Kotlin name reads as, through the + # module -- the proxy layer's function, this layer's overload set, a live + # constant -- so the two spellings are one object. Frozen into the module dict + # only when the Kotlin name itself is (a constant is read every time), and + # recorded as this layer's, so a table change drops it with everything else. + adapted = getattr(module, kotlin) + if module.__dict__.get(kotlin) is adapted: + setattr(module, name, adapted) + module._kotlin_adapted[name] = adapted + return adapted raise AttributeError( "module '" + module.__name__ + "' has no attribute '" + name + "' (nothing named that is exposed from Kotlin package " + kotlin_package + ')' @@ -2053,10 +2234,11 @@ object PythonxAdapter { def __dir__(): names = set(_BY_PACKAGE.get(kotlin_package, {})) names.update(_CHILDREN.get(kotlin_package, ())) - for type_name in _BY_RECEIVER: - package, _, leaf = type_name.rpartition('.') - if package == kotlin_package: - names.add(leaf) + for served in (_BY_RECEIVER, _METHODS): + for type_name in served: + package, _, leaf = type_name.rpartition('.') + if package == kotlin_package: + names.add(leaf) return sorted(names) return __dir__ diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/KotlinSurface.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/KotlinSurface.kt index 812ebacad..1ebb5fa17 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/KotlinSurface.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/KotlinSurface.kt @@ -8,12 +8,25 @@ import python.multiplatform.reflection.ExposedCallable * ### Why it exists * * A Kotlin-named module (`androidx.compose.material3`, `kotlin.text`, a consumer's own package) - * carries **Kotlin's own surface**: Kotlin declaration names, keyword arguments by **Kotlin - * parameter names**, Kotlin defaults for what a call leaves out. Nothing is renamed -- no - * snake_case, no `pythonx` -- because a Kotlin name in Python means the original Kotlin - * (`docs/INTENT.md` §2.2). Making that surface Pythonic is the business of a real Python package - * built on top of it (pythonx-compose, §2.3), and this module is what such a package reads to do - * it by rule rather than by a wrapper per declaration. + * is importable under its Kotlin package name and nothing else: **a namespace is never renamed**, + * no `pythonx` module is made (`docs/INTENT.md` §2.2, §2.3). Inside it, each declaration is reached + * by its Kotlin name **and** by its Pythonic name, and takes keyword arguments by **either** spelling + * of each parameter (issue #131): + * + * | Kotlin | also reachable as | rule | + * |---|---|---| + * | `rememberTextFieldState` | `remember_text_field_state` | snake_case of a lower-case-first name | + * | `toURLString` | `to_url_string` | a run of capitals is one word | + * | `paddingFromBaseline__TextUnit` | `padding_from_baseline__TextUnit` | an explicit overload key keeps its suffix | + * | `Checkbox`, `Modifier`, `Alignment.End` | (unchanged) | upper-case first: types, objects, composables, constants | + * | `onCheckedChange=` | `on_checked_change=` | a keyword is snake_case of the Kotlin parameter name | + * + * The rule is pythonx-compose 0.1.0a1's (`_reexport.py`'s `snake_case`/`python_name`), so a package + * built on top sees the same spelling it always computed. An alias is served only when it is + * unambiguous in its namespace -- one module, one proxy type (supertypes included), or one + * declaration's parameters: it must not already be a Kotlin name there and no second Kotlin name may + * map to it ([SOURCE]'s `pythonic_aliases`). An ambiguous alias is not served, and every Kotlin name + * involved stays reachable as itself. `dir()` lists Kotlin names only. * * Two installers put functions into those modules -- [PythonProxySource] renders every table entry * eagerly; [python.multiplatform.ffi.pythonx.PythonxAdapter] adapts names lazily, adds overload @@ -28,11 +41,19 @@ import python.multiplatform.reflection.ExposedCallable * | `describe_member(type, name)` | the declaration(s) a member name of a Kotlin type stands for on its proxy -- an extension function or a property, supertypes included -- read from the table | * | `kotlin_function(raw, row)` | what [PythonProxySource] wraps each rendered function in: keyword arguments by Kotlin name, omitted defaults, and the metadata above | * | `signature_of(rows)` | the `inspect.Signature` for one declaration (`(*args, **kwargs)` for an overload set) | + * | `snake_case(name)`, `python_name(name)` | the naming rule below, the one both installers and the stub generator apply | + * | `pythonic_aliases(names)` | `{alias: kotlin_name}` for one namespace, ambiguous aliases left out | + * | `keyword_slots(row)` | `{keyword: slot}` for one declaration: Kotlin parameter names and their aliases | + * | `module_alias_getattr(module)` | the PEP 562 `__getattr__` [PythonProxySource] gives the modules it creates, serving the aliases | * * ### The public contract, for a Pythonic layer * * Every function the binder puts on a Kotlin-named module -- whichever installer put it there -- - * answers `inspect.signature(fn)` with the **Kotlin** parameter names, in declaration order: + * answers `inspect.signature(fn)` with the **Pythonic** keyword of each parameter (its snake_case + * alias, or its Kotlin name where none is served), in declaration order. `describe()` carries both: + * `name` is the Kotlin parameter name, `python_name` the keyword the signature shows. The signature + * is the call surface Python tools read (`help()`, IDEs, `Signature.bind`), so it shows the spelling + * Python code is written in; the description is the Kotlin declaration, so it keeps Kotlin's. * * - an extension receiver is a positional-only parameter named `receiver`; * - a parameter with a Kotlin default has `default is python_multiplatform.KOTLIN_DEFAULT`; @@ -53,8 +74,9 @@ import python.multiplatform.reflection.ExposedCallable * 'composable': True, # has a $composer slot * 'content': None, # name of a trailing @Composable lambda parameter * 'parameters': ( - * {'name': 'checked', 'type': 'kotlin.Boolean', 'tag': 'BOOLEAN', + * {'name': 'checked', 'python_name': 'checked', 'type': 'kotlin.Boolean', 'tag': 'BOOLEAN', * 'has_default': False, 'value_class': False, 'composable_lambda': False}, + * {'name': 'onCheckedChange', 'python_name': 'on_checked_change', ...}, * ...)} * * `python_multiplatform.describe(module, name)` describes **any** bound name of a Kotlin-named @@ -83,6 +105,8 @@ import python.multiplatform.reflection.ExposedCallable * - a name the type does not have is looked up on each type the table says it **is a**, nearest * first -- the same lookup the proxy uses, so a description cannot disagree with the attribute; * - nothing is invoked: no getter runs, no handle is resolved; + * - a member's Pythonic alias (`fill_max_width`) describes the Kotlin member it is served for, and so + * does `describe(module, alias)` for a module name; * - a name served on neither the type nor any supertype, or any name while the binding layer is not * installed (it is what serves members on a proxy), raises `AttributeError`. * @@ -90,9 +114,12 @@ import python.multiplatform.reflection.ExposedCallable * * A Kotlin-named module's `dir()` lists its bound declarations, the receiver types under it, and * its **direct child packages and objects** (an object whose members are bound, `Alignment`, is a - * package here). Each under its Kotlin name. Reading a child as an attribute imports it, so - * `androidx.compose.ui.Alignment.End` works after `import androidx` alone; a name nothing is bound - * under stays an `AttributeError`. + * package here). Each under its Kotlin name only -- the Pythonic aliases are served but not listed, + * so `dir()` names each declaration once (and a layer that converts `dir()` by the same rule, as + * pythonx-compose 0.1.0a1's `_name_table` does, sees no two entries for one name). Reading a child + * as an attribute imports it, so `androidx.compose.ui.Alignment.End` works after `import androidx` + * alone; a name nothing is bound under stays an `AttributeError`. A package segment is a namespace + * and is never given an alias. * * `value_class` is true for a parameter whose marshalling tag is a primitive while its declared * type is not a Kotlin primitive (`Dp`, `Color`, `TextUnit`) -- the only machine-checkable form of @@ -101,7 +128,10 @@ import python.multiplatform.reflection.ExposedCallable * ### Member resolvers: the one hook for names on a proxy * * A proxy the binder returns (`Modifier.padding(16)` is one) serves its Kotlin members under their - * Kotlin names only. A Pythonic package built on the binder can say what else a member may be called: + * Kotlin names and their Pythonic aliases. The order a name is looked up in is fixed: **the Kotlin + * member of that name, then the registered resolvers, then the binder's own snake_case alias** -- so + * a resolver can still decide what a name means, and with none registered the alias is served. A + * Pythonic package built on the binder can say what else a member may be called: * * python_multiplatform.binding.add_member_resolver(fn) * python_multiplatform.binding.remove_member_resolver(fn) @@ -113,20 +143,23 @@ import python.multiplatform.reflection.ExposedCallable * never reaches it. `kotlin_member_names` is a tuple of the Kotlin member names the type has. * - The first resolver (in registration order) returning a name that is in `kotlin_member_names` wins, * and that Kotlin member is served. Any other answer (`None`, an unknown name, a name starting with - * `_`) means "not mine"; with no resolver answering, the result is the usual `AttributeError`. + * `_`) means "not mine"; with no resolver answering, the binder's own alias is served, and a name + * that is no alias either is the usual `AttributeError`. * - **Keyword maps.** An answer may be `(kotlin_name, keyword_map)`, `keyword_map` being * `{python_kw: kotlinParam}`: it is applied when that member is called, and keywords it does not - * name pass through as Kotlin parameter names (an unknown one is still the binding layer's - * `TypeError`, which lists the Kotlin parameters). For a member the proxy *does* have under its + * name pass through unchanged, to be matched as a Kotlin parameter name or its Pythonic alias (an + * unknown one is still the binding layer's `TypeError`). A map that turns `padding_values` into + * `paddingValues` is therefore harmless: the binder would have accepted either. For a member the proxy *does* have under its * Kotlin name (`padding`), resolvers are asked only when a call to it passes keyword arguments, and * only for its keyword map: an answer counts when it is `(that same name, keyword_map)`; a plain * name or `None` means no map. A call with no keywords never asks. A tuple of any other shape is a * `TypeError`. - * - The binder renames nothing itself: with no resolver registered behaviour is exactly the - * Kotlin-names-only behaviour above. + * - With no resolver registered, a proxy serves its Kotlin names and the binder's aliases (issue #131). * - Answers are cached in the resolver registry (cleared when a resolver is added or removed, and * when a table is registered), never written onto a proxy class: `dir()` of a proxy and the class - * `__dict__` show Kotlin names only. Removing a resolver removes its aliases. + * `__dict__` show Kotlin names only. Removing a resolver removes its aliases. The binder's own + * aliases follow the same rule: computed per type, cached beside the table, never written onto the + * class, so reading an alias costs a `__getattr__` miss every time. * - Registering the same function twice is a no-op. Resolvers are process-wide, like the binding layer. * * ### `__signature__` is lazy @@ -162,6 +195,7 @@ object KotlinSurface { import inspect as _inspect import keyword as _keyword + import re as _re import sys as _sys _pm_root = True @@ -226,6 +260,135 @@ object KotlinSurface { return name.startswith('${'$'}') + # ------------------------------------------------------------- Pythonic names (issue #131) + # + # A Kotlin-named module is reachable under its Kotlin package name and nothing else -- a + # namespace is never renamed (`docs/INTENT.md` §2.2). Inside it, every declaration, member and + # keyword parameter is reachable by its Kotlin name **and** by its Pythonic name. The rule is + # the one pythonx-compose 0.1.0a1 uses (`pythonx/compose/_reexport.py`), character for + # character, so a package built on top sees no change of spelling: + # + # - a name that starts upper-case (a type, an object, a `@Composable`, an enum entry, a + # PascalCase constant) is unchanged; + # - any other name is snake_case, a run of capitals counting as one word + # (`fillMaxWidth` -> `fill_max_width`, `toURLString` -> `to_url_string`); + # - an explicit overload key converts its base and keeps its `__Types` suffix + # (`paddingFromBaseline__TextUnit` -> `padding_from_baseline__TextUnit`); + # - a keyword parameter is `snake_case` of its Kotlin name (`onCheckedChange` -> `on_checked_change`). + # + # An alias is served only when it is unambiguous in its namespace (`pythonic_aliases`): when it + # is not already some Kotlin name there, and when no second Kotlin name maps to it. Otherwise + # the Kotlin names are still the only spellings, and nothing is guessed. + + _LOWER_UPPER = _re.compile(r'([a-z0-9])([A-Z])') + _ACRONYM_WORD = _re.compile(r'([A-Z]+)([A-Z][a-z])') + + + def snake_case(kotlin_name): + '''`fillMaxWidth` -> `fill_max_width`, `toURLString` -> `to_url_string`, `zIndex` -> `z_index`.''' + return _LOWER_UPPER.sub(r'\1_\2', _ACRONYM_WORD.sub(r'\1_\2', kotlin_name)).lower() + + + def python_name(kotlin_name): + '''The one Pythonic spelling of a Kotlin declaration or member name. + + Upper-case names keep their Kotlin spelling. An explicit overload key (`padding__Dp`) + converts its base and keeps the type suffix, which names Kotlin types. + ''' + if not kotlin_name or kotlin_name[:1].isupper(): + return kotlin_name + base, sep, suffix = kotlin_name.partition('__') + return snake_case(base) + sep + suffix + + + def pythonic_aliases(kotlin_names, rule=python_name): + '''`{alias: kotlin_name}` for the names of one namespace, ambiguity refused. + + An alias is served when it differs from its Kotlin name, is a Python identifier, is not + itself one of [kotlin_names] (that Kotlin name owns it), and exactly one Kotlin name maps + to it. Two Kotlin names that map to one alias (`toURL` and `toUrl` -> `to_url`) serve no + alias at all: both stay reachable by their Kotlin names only. Independent of order. + ''' + names = set(kotlin_names) + found = {} + clashed = set() + for kotlin in names: + if not isinstance(kotlin, str): + continue + alias = rule(kotlin) + if alias == kotlin or alias in names or not alias.isidentifier(): + continue + other = found.get(alias) + if other is None: + found[alias] = kotlin + elif other != kotlin: + clashed.add(alias) + for alias in clashed: + del found[alias] + return found + + + def _parameter_aliases(names): + '''`{kotlin_parameter: pythonic_keyword}` for one declaration's own parameter names.''' + own = [name for name in names if name != _RECEIVER and not _is_synthetic(name)] + return {kotlin: alias for alias, kotlin in pythonic_aliases(own, snake_case).items()} + + + def keyword_slots(row): + '''`{keyword: slot}` for one table row: each Kotlin parameter name, and its Pythonic alias. + + The receiver and the synthetic Compose slots answer to no keyword. An alias that collides + with another parameter's Kotlin name, or that two parameters share, is not served + (`pythonic_aliases`); those parameters are reached by their Kotlin names. + ''' + names = row[_NAMES] or () + index = {} + for slot, name in enumerate(names): + if name == _RECEIVER or _is_synthetic(name): + continue + index.setdefault(name, slot) + for kotlin, alias in _parameter_aliases(names).items(): + index.setdefault(alias, index[kotlin]) + return index + + + def _module_aliases(module): + '''`{alias: kotlin_name}` for the names the proxy layer published on [module]. + + Recomputed when the proxy layer renders a new table or the module gains a name, so an alias + always reads the module's *current* Kotlin attribute and never a value cached from an + older render. + ''' + names = [name for name in module.__dict__ if not name.startswith('_')] + rows = getattr(type(module), '_pm_kotlin_rows', None) or {} + names.extend(name for name in rows if name not in module.__dict__) + key = (_proxy_epoch, len(names)) + cached = module.__dict__.get('_pm_aliases') + if cached is not None and cached[0] == key: + return cached[1] + aliases = pythonic_aliases(names) + module.__dict__['_pm_aliases'] = (key, aliases) + return aliases + + + def module_alias_getattr(module): + '''The PEP 562 `__getattr__` the proxy layer gives a module it creates: Pythonic aliases. + + Asked only for a name the module does not have. The binding layer, when it adapts the + module, replaces this with its own `__getattr__`, which serves the same aliases from the + table. The alias is never written into the module: every read goes to the Kotlin name. + ''' + def __getattr__(name): + if name.startswith('__') and name.endswith('__'): + raise AttributeError(name) + kotlin = _module_aliases(module).get(name) + if kotlin is None: + raise AttributeError("module '" + module.__name__ + "' has no attribute '" + name + "'") + return getattr(module, kotlin) + + return __getattr__ + + def _is_composable_lambda(type_name): if not type_name or not type_name.startswith(_FUNCTION_PREFIX): return False @@ -243,6 +406,7 @@ object KotlinSurface { tags = row[_TAGS] or () types = row[_TYPES] or () defaults = row[_DEFAULTS] or () + aliases = _parameter_aliases(names) parameters = [] for index in range(row[_ARITY]): name = _field(names, index) @@ -252,6 +416,9 @@ object KotlinSurface { tag = _field(tags, index) parameters.append({ 'name': name, + # The keyword `inspect.signature` shows for it: its snake_case alias, or its Kotlin + # name where no alias is served (issue #131). Both are accepted by a call. + 'python_name': aliases.get(name, name), 'type': type_name, 'tag': tag, 'has_default': bool(_field(defaults, index)), @@ -312,11 +479,16 @@ object KotlinSurface { # is kept on the module's type (`PythonProxySource._pm_kotlin_rows`), and a rendered # function is found by `getattr_static`, which runs no descriptor -- so a constant's # Kotlin getter is never invoked here. - row = (getattr(type(module), '_pm_kotlin_rows', None) or {}).get(name) + kotlin = name + row = (getattr(type(module), '_pm_kotlin_rows', None) or {}).get(kotlin) + if row is None and not name.startswith('_'): + # A Pythonic alias describes the Kotlin declaration it is served for (issue #131). + kotlin = _module_aliases(module).get(name, name) + row = (getattr(type(module), '_pm_kotlin_rows', None) or {}).get(kotlin) if row is not None: rows = (row,) else: - rows = getattr(_inspect.getattr_static(module, name, None), '__kotlin_rows__', None) + rows = getattr(_inspect.getattr_static(module, kotlin, None), '__kotlin_rows__', None) if not rows: raise AttributeError( "module '" + module.__name__ + "' has no bound Kotlin declaration named '" + name + "'" @@ -358,6 +530,10 @@ object KotlinSurface { def signature_of(rows, drop_receiver=False): '''The `inspect.Signature` of one declaration; `(*args, **kwargs)` for anything else. + Each parameter is shown under its **Pythonic** keyword (`on_checked_change`), or under its + Kotlin name where no alias is served (issue #131); a call accepts both spellings, and + `describe` carries both (`name`, `python_name`). + An overload set has no single signature, and a declaration whose Kotlin parameter name Python cannot spell (`in`, `is`, `from` ...) cannot be written as one -- its keyword is still accepted through `**{...}`, and `describe` still carries the true name. @@ -376,7 +552,12 @@ object KotlinSurface { if drop_receiver and params: params = params[1:] return _inspect.Signature(params, return_annotation=_annotation(_declared_return(row[_RTYPE]))) - receiver_name = 'receiver' if 'receiver' not in names else '_receiver' + aliases = _parameter_aliases(names) + shown = {} + for name in names: + alias = aliases.get(name) + shown[name] = alias if alias is not None and not _keyword.iskeyword(alias) else name + receiver_name = 'receiver' if 'receiver' not in shown.values() else '_receiver' kind = P.POSITIONAL_OR_KEYWORD seen_default = False for index, name in enumerate(names): @@ -387,6 +568,7 @@ object KotlinSurface { if not drop_receiver: params.append(P(receiver_name, P.POSITIONAL_ONLY, annotation=annotation)) continue + name = shown[name] if not name.isidentifier() or _keyword.iskeyword(name): return _GENERIC has_default = bool(_field(defaults, index)) @@ -436,8 +618,11 @@ object KotlinSurface { _MISSING = _Missing() - def bind_slots(row, args, kwargs): - '''Positional and Kotlin-named keyword arguments -> one value per slot of [row]. + def bind_slots(row, args, kwargs, keywords=None): + '''Positional and keyword arguments -> one value per slot of [row]. + + A keyword is a Kotlin parameter name or its Pythonic alias ([keywords], `keyword_slots` + when not given); writing both spellings of one parameter is "multiple values". A defaulted slot the call left out (or filled with `KOTLIN_DEFAULT`) becomes `None`: the walked Kotlin body tests `args[i] == null` and takes a call expression that does not @@ -457,13 +642,10 @@ object KotlinSurface { if len(args) > arity: raise TypeError(name + '() takes ' + str(arity) + ' arguments, got ' + str(len(args))) slots = list(args) + [_MISSING] * (arity - len(args)) + if kwargs and keywords is None: + keywords = keyword_slots(row) for key, value in kwargs.items(): - index = -1 - if key != _RECEIVER and not _is_synthetic(key): - for slot, declared in enumerate(names): - if declared == key: - index = slot - break + index = keywords.get(key, -1) if index < 0: raise TypeError(name + "() got an unexpected keyword argument '" + key + "'") if slots[index] is not _MISSING: @@ -492,7 +674,8 @@ object KotlinSurface { answer is the same whichever installer ran first; across tables the newer install wins, and neither layer's handles from an older table are ever used; - otherwise a call that fills every slot positionally goes straight to [raw], and any - other call is mapped onto the slots by Kotlin parameter name first (`bind_slots`). + other call is mapped onto the slots by Kotlin parameter name or Pythonic alias first + (`bind_slots`; the keyword map is built on the first call that passes a keyword). ''' name = row[_NAME] arity = row[_ARITY] @@ -500,6 +683,7 @@ object KotlinSurface { has_defaults = any(row[_DEFAULTS] or ()) modules = _sys.modules default = KOTLIN_DEFAULT + keywords = [] def kotlin(*args, **kwargs): binding = modules.get(BINDING_MODULE) @@ -515,7 +699,9 @@ object KotlinSurface { break else: return raw(*args) - return raw(*bind_slots(row, args, kwargs)) + if kwargs and not keywords: + keywords.append(keyword_slots(row)) + return raw(*bind_slots(row, args, kwargs, keywords[0] if keywords else None)) kotlin.__wrapped__ = _SignatureCarrier(raw, (row,)) kotlin.__kotlin_rows__ = (row,) diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/PythonProxySource.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/PythonProxySource.kt index b17fd4184..b32203d30 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/PythonProxySource.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/upcall/PythonProxySource.kt @@ -230,7 +230,7 @@ import python.multiplatform.reflection.UpcallTable * A class with static members already has a metaclass, and the two requirements do not both fit in * a base list: Python has no syntax for a base *after* a keyword, so the only spelling available is * `class Foo(_PmObject, metaclass=_pm_t_1):` -- which separates the class from the metaclass a - * reader is pairing it with, and did more than that. `ksp-fixtures:app` locates a companion by + * reader is pairing it with, and did more than that. `python-multiplatform-ksp:fixtures:app` locates a companion by * matching `class (_pm_t_\d+)\(type\):` lazily through to `class Foo\(metaclass=\1\):`, and once * that terminator no longer existed the match could not complete: `java.util.regex`'s lazy loop * recurses once per character, so it scanned the rest of a 30 KB module and raised @@ -299,6 +299,10 @@ object PythonProxySource { # Kotlin defaults for what a call leaves out, and the signature as public metadata. See # `KotlinSurface`. from python_multiplatform import kotlin_function as _pm_kotlin_function + # Each module this layer creates also answers the Pythonic alias of a Kotlin name it holds + # (`remember_text_field_state` for `rememberTextFieldState`, issue #131), through a PEP 562 + # `__getattr__` that runs only for a name the module does not have. See `KotlinSurface`. + from python_multiplatform import module_alias_getattr as _pm_module_alias_getattr def _pm_settle(_fut, _ok, _payload): @@ -351,6 +355,10 @@ object PythonProxySource { _m = _pm_sys.modules.get(_name) if _m is None: _m = _pm_types.ModuleType(_name) + # Only on a module this layer creates: one the binding layer made already has its + # own `__getattr__`, which serves the same aliases from its table, and a module + # anybody else made is theirs. + _m.__getattr__ = _pm_module_alias_getattr(_m) _pm_sys.modules[_name] = _m if '.' in _name: _parent, _, _leaf = _name.rpartition('.') diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/reflection/InstallsUpcallTable.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/reflection/InstallsUpcallTable.kt index 358421489..cd55edc82 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/reflection/InstallsUpcallTable.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/reflection/InstallsUpcallTable.kt @@ -12,7 +12,7 @@ package python.multiplatform.reflection * * e: TableInstall.kt: Unresolved reference 'FunctionTable'. * - * observed on `ksp-fixtures/app`'s `commonMain`. ROADMAP §13 recorded the consequence: every + * observed on `python-multiplatform-ksp/fixtures/app`'s `commonMain`. ROADMAP §13 recorded the consequence: every * consumer wrote the same one-line `actual` once per leaf target, and shared code that wanted the * table had nothing to call. `androidMain` is the exception that proves the rule -- it *is* the * Android target's own source set, so it can name `FunctionTable` -- and an exception per platform diff --git a/python-multiplatform/src/commonMain/kotlin/python/native/ffi/EmbedAPI.kt b/python-multiplatform/src/commonMain/kotlin/python/native/ffi/EmbedAPI.kt index 5903e9daf..d73267082 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/native/ffi/EmbedAPI.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/native/ffi/EmbedAPI.kt @@ -288,8 +288,8 @@ expect inline fun Py_GetBuildInfo(): String? * * This is no longer bound to a C symbol. `ceval.h` marks it `Py_DEPRECATED(3.9)`, which is a * compiler-enforced deprecation and therefore a removal schedule, and since 3.9 the C function's - * body has been empty. A Kotlin no-op is not an approximation of it — it is the same behaviour with - * one fewer FFI crossing — so nothing is lost by taking it out of the `expect`/`actual` surface, + * body has been empty. A Kotlin no-op is not an approximation of it, it is the same behaviour with + * one fewer FFI crossing, so nothing is lost by taking it out of the `expect`/`actual` surface, * and callers written against older embeddings still compile. `EmbedApiSurfaceTest` derives the * `Py_DEPRECATED` set from the bundled headers and fails if this name reappears in the surface. */ @@ -1287,7 +1287,7 @@ expect fun PyImport_ImportModule(name: String): NativePointer? * [PyImport_ImportModule] instead. * * This is no longer bound to a C symbol. CPython 3.15 removed the entry point, and since 3.3 - * it had been an exact alias of `PyImport_ImportModule` — not a distinct code path — so + * it had been an exact alias of `PyImport_ImportModule`, not a distinct code path, so * forwarding here is behaviour-preserving on every supported version rather than an * approximation. Keeping the name costs nothing at the FFI boundary and lets existing source * compile through the version bump; `EmbedApiLowLevelTest` pins it to the function it aliases. @@ -1537,7 +1537,7 @@ expect fun PyImport_GetImporter(path: NativePointer): NativePointer? * the module is not found, and "-1" with an exception set if the * initialization failed. To access the imported module on a * successful load, use "PyImport_ImportModule()". (Note the misnomer - * — this function would reload the module if it was already + *, this function would reload the module if it was already * imported.) * * Added in version 3.3. @@ -4133,16 +4133,16 @@ expect fun PyWeakref_NewProxy(ob: NativePointer, callback: NativePointer): Nativ * with [Py_DecRef]. Returns `null` if the referent is no longer live, and also `null` if *ref* * is not a weak reference object. * - * The C function distinguishes those two outcomes by its `int` return — `0` for a dead referent - * and `-1` for an error — while writing the object through an out-parameter. An out-parameter + * The C function distinguishes those two outcomes by its `int` return, `0` for a dead referent + * and `-1` for an error, while writing the object through an out-parameter. An out-parameter * does not survive this FFI boundary (Android requires primitives only across JNI), so both * arrive here as `null`. No information is lost: the error case sets the error indicator and the * dead case does not, so [PyErr_Occurred] separates them. `EmbedApiLowLevelTest` asserts that a * dead referent leaves the indicator clear. * * This replaces `PyWeakref_GetObject`, which CPython removed in 3.15. The two are not - * interchangeable: the removed function returned a *borrowed* reference and `Py_None` — not - * `null` — for a dead referent. `PyWeakref_GetRef` exists in 3.13 and 3.14 as well, so binding + * interchangeable: the removed function returned a *borrowed* reference and `Py_None`, not + * `null`, for a dead referent. `PyWeakref_GetRef` exists in 3.13 and 3.14 as well, so binding * it does not cost support for the version this build defaults to. */ expect fun PyWeakref_GetRef(ref: NativePointer): NativePointer? diff --git a/python-multiplatform/src/commonMain/python/python_multiplatform_devagent.py b/python-multiplatform/src/commonMain/python/python_multiplatform_devagent.py new file mode 100644 index 000000000..42aed8942 --- /dev/null +++ b/python-multiplatform/src/commonMain/python/python_multiplatform_devagent.py @@ -0,0 +1,372 @@ +"""The dev agent: a debug-only channel from a notebook or a hot reload into the running app's interpreter. + +python-multiplatform SPEC L-12, issue #233; the design is toolchain's docs/design/live-connection.md. + +The runtime starts it from `Python3.initialize()` only when the launcher sets the environment variable +`PYTHON_MULTIPLATFORM_DEV_AGENT` to `port=;token=[;overlay=]` (a debug launch does; a release +build never does), and only in a build that staged the enabling marker (the runtime checks that; see +`PythonDevAgent.kt`). It listens on 127.0.0.1 only. The token must be at least `MIN_TOKEN_LENGTH` characters +(`secrets.token_urlsafe(32)`), or the agent refuses to start. A connection's first message must be `hello` +with the token; anything else closes the connection. + +Messages are JSON objects, one per line, each carrying the request's `id`: + +- `{"op": "hello", "token": t}` -> `{"type": "hello", "protocol": 1, "python": ..., "pid": ...}` +- `{"op": "execute", "code": src}` -> any number of `{"type": "stream", "name": "stdout"|"stderr", "text"}`, + a `{"type": "result", "data": {"text/plain": repr}}` when the last statement is an expression whose value + is not None, then `{"type": "done", "status": "ok"|"error", "count": n}` (on error also `ename`, `evalue`, + `traceback`). The code runs in `__main__`'s namespace, which persists across requests. +- `{"op": "complete", "code": src, "cursor_pos": n}` -> `done` with `matches`, `cursor_start`, `cursor_end`. +- `{"op": "push", "files": {relative path: text}}` -> the files are written into the overlay, a writable + directory first on `sys.path` (an app's payload can be read-only: a desktop jar, an iOS bundle), and every + already-imported module among them is re-imported, children first. `done` lists them in `reloaded`. If + `typedpython.importgate` is in the app, the gate covers the overlay (SPEC N-7b), so a module that does not + type-check is refused; a refused or failing re-import leaves the old module in place. + +Stdlib only: it has to run inside any app, on every platform python-multiplatform supports. +""" +import ast +import hmac +import importlib +import importlib.util +import json +import os +import rlcompleter +import socket +import sys +import tempfile +import threading +import traceback + +PROTOCOL = 1 +ENVIRONMENT_VARIABLE = "PYTHON_MULTIPLATFORM_DEV_AGENT" +LOOPBACK = "127.0.0.1" +# len(secrets.token_urlsafe(32)): 32 random bytes. The token is all that stands between any local process +# and code execution in the app, so a short one is refused rather than used. +MIN_TOKEN_LENGTH = 43 + + +def parse_request(value): + """`port=;token=[;overlay=]` -> `{"port", "token", "overlay"}`; ValueError when malformed.""" + fields = {} + for part in (value or "").split(";"): + if "=" in part: + key, _, item = part.partition("=") + fields[key.strip()] = item.strip() + try: + port = int(fields["port"]) + token = fields["token"] + except (KeyError, ValueError): + raise ValueError(f"{ENVIRONMENT_VARIABLE} must be port=;token=[;overlay=], not {value!r}") + if not 0 <= port <= 65535: + raise ValueError(f"{ENVIRONMENT_VARIABLE} must be port=;token=[;overlay=], not {value!r}") + _check_token(token) + return {"port": port, "token": token, "overlay": fields.get("overlay") or None} + + +def start_from_env(): + """Start an agent when the launcher asked for one; return it, or None when it did not.""" + value = os.environ.get(ENVIRONMENT_VARIABLE) + if not value: + return None + request = parse_request(value) + agent = start(port=request["port"], token=request["token"], overlay=request["overlay"]) + print(f"python-multiplatform dev agent: listening on {agent.host}:{agent.port}", file=sys.stderr, flush=True) + return agent + + +def _check_token(token): + if not isinstance(token, str) or len(token) < MIN_TOKEN_LENGTH: + raise ValueError( + f"the dev agent's token must be at least {MIN_TOKEN_LENGTH} characters " + "(secrets.token_urlsafe(32)); refusing to start" + ) + + +def start(port, token, overlay=None): + """Start an agent on loopback; `port=0` picks a free one (the agent's `port` says which).""" + _check_token(token) + agent = Agent(token, overlay or tempfile.mkdtemp(prefix="python-multiplatform-overlay-")) + agent.listen(port) + return agent + + +class _Stream: + """Replaces sys.stdout or sys.stderr during a request: every write goes to the client.""" + + def __init__(self, connection, request_id, name): + self.connection = connection + self.request_id = request_id + self.name = name + + def write(self, text): + if text: + self.connection.send({"id": self.request_id, "type": "stream", "name": self.name, "text": text}) + return len(text) + + def flush(self): + pass + + def isatty(self): + return False + + +class Agent: + def __init__(self, token, overlay): + self.token = token + self.overlay = os.path.abspath(overlay) + os.makedirs(self.overlay, exist_ok=True) + if self.overlay not in sys.path: + sys.path.insert(0, self.overlay) + self.namespace = sys.modules["__main__"].__dict__ + self.count = 0 + # One request at a time across connections: requests share __main__, sys.stdout and the modules. + self.lock = threading.Lock() + self.host = LOOPBACK + self.port = None + self.server = None + self.connections = [] + self.gate = self._install_gate() + + def _install_gate(self): + try: + from typedpython import importgate + except ImportError: + return None + importgate.install([self.overlay]) + return importgate + + def listen(self, port): + self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if hasattr(socket, "SO_EXCLUSIVEADDRUSE"): + # Windows: SO_REUSEADDR there lets another socket bind the port while this one listens, and + # without SO_EXCLUSIVEADDRUSE a same-user process can do so even when this one did not set it. + self.server.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) + else: + # POSIX: only so that a restarted app can bind the launcher's port while the last run's + # connections sit in TIME_WAIT. It does not let another socket bind 127.0.0.1: while this + # one listens (Linux refuses a second listener; BSD and macOS need SO_REUSEPORT on both). + # test_another_socket_cannot_take_the_port_over checks that on each platform the tests run on. + self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server.bind((LOOPBACK, port)) + self.server.listen() + self.port = self.server.getsockname()[1] + self.accept_thread = threading.Thread(target=self._accept, name="python-multiplatform-devagent", daemon=True) + self.accept_thread.start() + + def stop(self): + """Stop listening and close every connection; the port is free when this returns.""" + # On Linux, close() alone leaves a socket that another thread is blocked accepting on in LISTEN, so the + # port stays taken (a restarted agent's bind failed with EADDRINUSE there). shutdown() wakes accept(). + # macOS refuses shutdown() on a listening socket (ENOTCONN), and there close() is enough. + try: + self.server.shutdown(socket.SHUT_RDWR) + except OSError: + pass + try: + self.server.close() + except OSError: + pass + connections = list(self.connections) + for connection in connections: + connection.shutdown() + current = threading.current_thread() + for thread in [self.accept_thread] + [c.thread for c in connections]: + if thread is not None and thread is not current: + thread.join(timeout=5) + + def _accept(self): + while True: + try: + sock, _ = self.server.accept() + except OSError: + return + connection = _Connection(self, sock) + self.connections.append(connection) + connection.thread = threading.Thread(target=connection.serve, name="python-multiplatform-devagent-client", daemon=True) + connection.thread.start() + + +class _Connection: + def __init__(self, agent, sock): + self.thread = None + self.agent = agent + self.sock = sock + self.reader = sock.makefile("r", encoding="utf-8") + self.send_lock = threading.Lock() + self.authenticated = False + + def send(self, message): + data = (json.dumps(message, ensure_ascii=False) + "\n").encode("utf-8") + with self.send_lock: + self.sock.sendall(data) + + def shutdown(self): + """Wake the serving thread, which then closes the connection itself. + + Closing the reader from another thread would wait on the buffered reader's lock, which the serving + thread holds while it blocks in a read: stop() hung there. + """ + try: + self.sock.shutdown(socket.SHUT_RDWR) + except OSError: + pass + + def close(self): + for closable in (self.reader, self.sock): + try: + closable.close() + except OSError: + pass + if self in self.agent.connections: + self.agent.connections.remove(self) + + def serve(self): + try: + for line in self.reader: + if not line.strip(): + continue + if not self._handle(json.loads(line)): + break + except (OSError, ValueError): + pass + finally: + self.close() + + def _handle(self, message): + """Answer one request; False closes the connection.""" + request_id = message.get("id") + op = message.get("op") + if not self.authenticated: + token = message.get("token") if op == "hello" else None + if token is None or not hmac.compare_digest(str(token), self.agent.token): + self.send({"id": request_id, "type": "error", "ename": "AuthError", "evalue": "hello with the token first"}) + return False + self.authenticated = True + self.send({"id": request_id, "type": "hello", "protocol": PROTOCOL, "python": sys.version.split()[0], "pid": os.getpid()}) + return True + handler = {"hello": self._hello, "execute": self._execute, "complete": self._complete, "push": self._push}.get(op) + if handler is None: + self._done(request_id, error=("UnknownOp", f"unknown op {op!r}", [])) + return True + with self.agent.lock: + handler(request_id, message) + return True + + def _done(self, request_id, error=None, **fields): + reply = {"id": request_id, "type": "done", "status": "ok" if error is None else "error", **fields} + if error is not None: + reply["ename"], reply["evalue"], reply["traceback"] = error + self.send(reply) + + @staticmethod + def _error(exception): + return type(exception).__name__, str(exception), traceback.format_exception(type(exception), exception, exception.__traceback__) + + def _hello(self, request_id, message): + self.send({"id": request_id, "type": "hello", "protocol": PROTOCOL, "python": sys.version.split()[0], "pid": os.getpid()}) + + def _execute(self, request_id, message): + self.agent.count += 1 + count = self.agent.count + namespace = self.agent.namespace + stdout, stderr = sys.stdout, sys.stderr + sys.stdout = _Stream(self, request_id, "stdout") + sys.stderr = _Stream(self, request_id, "stderr") + try: + tree = ast.parse(message.get("code", ""), filename=f"", mode="exec") + last = tree.body.pop() if tree.body and isinstance(tree.body[-1], ast.Expr) else None + exec(compile(tree, f"", "exec"), namespace) + if last is not None: + value = eval(compile(ast.Expression(last.value), f"", "eval"), namespace) + if value is not None: + namespace["_"] = value + self.send({"id": request_id, "type": "result", "data": {"text/plain": repr(value)}}) + except BaseException as exception: # a cell's SystemExit or KeyboardInterrupt must not end the agent + sys.stdout, sys.stderr = stdout, stderr + self._done(request_id, error=self._error(exception), count=count) + return + finally: + sys.stdout, sys.stderr = stdout, stderr + self._done(request_id, count=count) + + def _complete(self, request_id, message): + code = message.get("code", "") + cursor = int(message.get("cursor_pos", len(code))) + before = code[:cursor] + start = cursor + while start > 0 and (before[start - 1].isalnum() or before[start - 1] in "_."): + start -= 1 + text = before[start:] + completer = rlcompleter.Completer(self.agent.namespace) + matches = [] + state = 0 + while True: + match = completer.complete(text, state) + if match is None: + break + matches.append(match.rstrip("(")) + state += 1 + self._done(request_id, matches=sorted(set(matches)), cursor_start=start, cursor_end=cursor) + + def _push(self, request_id, message): + overlay = self.agent.overlay + written = [] + try: + for relative, text in (message.get("files") or {}).items(): + normal = self._overlay_path(relative) + target = os.path.join(overlay, normal) + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, "w", encoding="utf-8") as f: + f.write(text) + # Bytecode is validated by mtime in seconds and size; a same-size edit within the second + # would load the stale .pyc, so it goes. + cached = importlib.util.cache_from_source(target) + if os.path.exists(cached): + os.remove(cached) + written.append(normal) + except (ValueError, OSError) as exception: + self._done(request_id, error=self._error(exception)) + return + importlib.invalidate_caches() + names = [self._module_name(path) for path in written] + loaded = sorted((n for n in names if n in sys.modules), key=lambda n: n.count("."), reverse=True) + reloaded = [] + for name in loaded: + try: + importlib.reload(sys.modules[name]) + except BaseException as exception: + self._done(request_id, error=self._error(exception), written=written, reloaded=reloaded) + return + reloaded.append(name) + self._done(request_id, written=written, reloaded=reloaded) + + def _overlay_path(self, relative): + """A pushed path, checked by the protocol's own rule rather than os.path's, as an OS path in the overlay. + + os.path differs per platform: on Windows `isabs("/x")` is False (Python 3.13+) and joining such a path to + the overlay lands on the drive root. So a path must be `/`-separated and relative, with no backslash, no + drive colon, and no empty, `.` or `..` segment, and must still resolve inside the overlay. + """ + segments = relative.split("/") if isinstance(relative, str) else [] + if ( + not segments + or "\\" in relative + or ":" in relative + or any(segment in ("", ".", "..") for segment in segments) + or not segments[-1].endswith(".py") + ): + raise ValueError(f"{relative!r} is not a relative /-separated .py path inside the overlay") + normal = os.path.join(*segments) + overlay = os.path.realpath(self.agent.overlay) + target = os.path.realpath(os.path.join(overlay, normal)) + if os.path.commonpath([overlay, target]) != overlay: + raise ValueError(f"{relative!r} resolves outside the overlay") + return normal + + @staticmethod + def _module_name(path): + parts = path[: -len(".py")].replace(os.sep, "/").split("/") + if parts[-1] == "__init__": + parts.pop() + return ".".join(parts) diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/VersionsTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/VersionsTest.kt index a1d992638..c64073c1b 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/VersionsTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/VersionsTest.kt @@ -8,7 +8,7 @@ import kotlin.test.assertTrue * Pins the version-string arithmetic that names the CPython artefacts on disk. * * The interesting case is the free-threaded flavour. A free-threaded install renames *everything* - * the runtime looks up by name — `libpython3.14t.dylib`, `lib/python3.14t/`, `bin/python3.14t` — + * the runtime looks up by name, `libpython3.14t.dylib`, `lib/python3.14t/`, `bin/python3.14t`, * and it does not ship the un-suffixed names at all. Getting [Versions.taggedVersionString] wrong * therefore does not degrade gracefully: `manager.loadLibPython` throws `UnsatisfiedLinkError` for * a file that was downloaded and is sitting right there under a different name. diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/env/InterpreterAvailabilityTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/env/InterpreterAvailabilityTest.kt index f43c7fc42..f3cd93be7 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/env/InterpreterAvailabilityTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/env/InterpreterAvailabilityTest.kt @@ -16,7 +16,7 @@ import kotlin.test.assertTrue * pointer-level measurements can run, and every functional test has to be skipped. * * The embedded `Python.framework` contains no standard library, so `PYTHONHOME` must point at - * a prefix holding `lib/python3.14`. The build wires that up for simulator test runs — see + * a prefix holding `lib/python3.14`. The build wires that up for simulator test runs, see * `extractIosSimulatorStdlib` in `build.gradle.kts`. */ class InterpreterAvailabilityTest { diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/PythonTestFixture.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/PythonTestFixture.kt index a31137892..6e672c57d 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/PythonTestFixture.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/PythonTestFixture.kt @@ -85,7 +85,7 @@ object PythonTestFixture { */ fun mainGlobals(): PyObject = Python3.withPython { // These reach the C API directly, so they need the GIL like any other call. The - // initialising thread no longer holds it — see Python3.initialize. + // initialising thread no longer holds it, see Python3.initialize. // PyImport_AddModule: *borrowed* reference, only read below and never wrapped. val modulePtr = python.multiplatform.ffi.Python3.withPython { PyImport_AddModule("__main__") } ?: error("Could not get __main__ module") // PyObject_GetAttrString: *new* reference, adopted by the wrapper. diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunAppTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunAppTest.kt index 340d646a8..8e87c76d0 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunAppTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunAppTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertTrue /** - * [Python3.runApp] — the decided, deliberately reduced subset of CPython's command-line parser + * [Python3.runApp], the decided, deliberately reduced subset of CPython's command-line parser * that an *embedder* can drive without calling `Py_BytesMain`/`Py_RunMain` (which finalize the * interpreter; see ROADMAP §12/§14b item 10 and [RunMainTest]'s header for why those cannot be the * implementation of anything called more than once). diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunMainTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunMainTest.kt index e163d9771..04ceb35dd 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunMainTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/RunMainTest.kt @@ -8,13 +8,13 @@ import kotlin.test.assertTrue import kotlin.time.TimeSource /** - * [Python3.runMain] — running a module as `__main__` in an interpreter that has to **survive** + * [Python3.runMain], running a module as `__main__` in an interpreter that has to **survive** * the call. * * The design question these tests fix an answer to is recorded in ROADMAP §12. The short form: * * - `Py_RunMain()` (and `Py_BytesMain()`, which reaches it) **always finalizes the interpreter** - * — `cpython/pylifecycle.h` declares it, and it is not in the Limited API at all. Neither can + * , `cpython/pylifecycle.h` declares it, and it is not in the Limited API at all. Neither can * be the implementation of anything an embedder calls more than once. * - `PyImport_ImportModule()` runs the module body under its *own* `__name__` and caches it in * `sys.modules`, so `if __name__ == "__main__":` never fires and a second call is a no-op. @@ -25,7 +25,7 @@ import kotlin.time.TimeSource * `alter_sys=True` is the part that makes the module observe a `__main__`-shaped world: * `runpy._run_module_code` runs the code in a **fresh** module namespace, installs it as * `sys.modules["__main__"]` for the duration, points `sys.argv[0]` at the module's origin, and - * restores both in `__exit__` — so the `__main__` this suite's other tests write globals into is + * restores both in `__exit__`, so the `__main__` this suite's other tests write globals into is * put back whether the module returns or raises. [theInterpreterAndItsMainNamespaceSurviveRunMain] * is the guard for that specifically. */ @@ -301,7 +301,7 @@ class RunMainTest { * `runMain` is not in the same cost class as `exec` and must not be reached for in a loop: * every call re-runs the import system's `find_spec`, recompiles or reloads the module's code * object and builds a fresh namespace. This prints the measured per-call cost of both rather - * than asserting a threshold — the number is machine- and load-dependent, and the point is + * than asserting a threshold, the number is machine- and load-dependent, and the point is * that it is visible in the log next to `exec`'s, not that it sits under some bound. * * The one assertion is directional and holds by construction (a module run does strictly more @@ -359,7 +359,7 @@ class RunMainTest { * A meta-path finder serving module source out of a dict, installed once. * * `runpy` goes through `importlib.util.find_spec` and then `spec.loader.get_code(...)` - * (runpy.py, `_get_module_details`), so a plain `sys.modules` entry is not enough — the + * (runpy.py, `_get_module_details`), so a plain `sys.modules` entry is not enough, the * spec has to be able to produce a code object. This is the smallest thing that can, and * being in-memory it works identically on every target this suite runs on. */ diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposableShapedFragment.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposableShapedFragment.kt index 79e746bac..f9a68f247 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposableShapedFragment.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposableShapedFragment.kt @@ -18,7 +18,7 @@ import python.multiplatform.reflection.TypeTag * `$default` bits a Python call sets, what goes in the slots it left out, and what `$changed` is. * * The real end of it -- `androidx.compose.material3.Text` drawing "hi" -- is - * `:ksp-fixtures:compose`'s `ComposableRenderTest`, which is desktop-only for exactly those reasons. + * `:python-multiplatform-compose:fixtures:compose`'s `ComposableRenderTest`, which is desktop-only for exactly those reasons. * * ### The shape, and why every field of it matters * diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposeShapedFragment.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposeShapedFragment.kt index 4ad98bdcd..fec320e1b 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposeShapedFragment.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/ComposeShapedFragment.kt @@ -11,7 +11,7 @@ import python.multiplatform.reflection.TypeTag * * ### Why a stand-in rather than the real jars * - * `WalkedArtifactComposeModifierTest` (in `ksp-fixtures/artifact`) already proves that the walker + * `WalkedArtifactComposeModifierTest` (in `python-multiplatform-gradle-plugin/fixtures/artifact`) already proves that the walker * produces these entries out of `foundation-layout-desktop-1.6.11.jar` and that Compose's own * `padding` runs when Python calls them. What it cannot do is run anywhere but desktop, because it * needs a JVM classpath. The adapter under test here is `commonMain` Python, so its tests belong in @@ -32,12 +32,13 @@ import python.multiplatform.reflection.TypeTag * | `padding__Dp_Dp` / `__Dp_Dp_Dp_Dp` again | omitting a defaulted argument, including one in the *middle* of the list | * | `fillMaxHeight` | the same, under a name with no overload set: omission with no dispatcher involved | * | `size__Dp` | the second link of a chain, so the return really is a receiver again | - * | `fillMaxWidth` | a receiver-only extension, reached under its Kotlin name and no other | - * | `zIndex` | a genuine `kotlin.Float` parameter beside `Dp` ones: same tag, different declared type | - * | `toURLString` | an acronym-bearing name, which once tested a snake_case rule and now only has to resolve as itself | + * | `fillMaxWidth` | a receiver-only extension, reached as `fillMaxWidth` and as its Pythonic alias `fill_max_width` | + * | `zIndex` | a genuine `kotlin.Float` parameter beside `Dp` ones: same tag, different declared type; and `z_index`, the case where a one-letter first segment must not be swallowed | + * | `toURLString` | a name the snake -> camel rule **cannot** invert (`to_url_string` -> `toUrlString`); only the forward conversion of the module's own names reaches it | * | `paddingFromBaseline__TextUnit` | the value-class reject list: a packed wrapper must refuse a raw number | * | `emptyModifier` | where a chain starts. Compose has no bound declaration for this; see [EMPTY_MODIFIER] | * | `Arrangement.Start` / `.End` | `kind = STATIC_GETTER`: a value behind a name, read as an attribute and not called | + * | `Color__Int` / `Color__Long` | one integral parameter at two widths: an int picks the overload its Kotlin literal would (#146) | */ object ComposeShapedFragment : FunctionTableFragment { @@ -94,6 +95,32 @@ object ComposeShapedFragment : FunctionTableFragment { paramTypeNames = listOf(MODIFIER), returnTypeName = "kotlin.String", ) { args -> (args[0] as StubModifier).describe() }, + // `Color(color: Int)` and `Color(color: Long)` (#146): the same TypeTag, two Kotlin widths. Kotlin + // types `0xFFFFFFFF` as a Long literal and `0x11223344` as an Int one, and picks accordingly. + ExposedCallable( + name = "androidx.compose.ui.graphics.Color__Int", + arity = 1, + paramTypes = listOf(TypeTag.INT), + returnType = TypeTag.STRING, + paramNames = listOf("color"), + paramTypeNames = listOf("kotlin.Int"), + returnTypeName = "kotlin.String", + ) { args -> + calls += "Color__Int" + "Int:${(args[0] as Number).toLong()}" + }, + ExposedCallable( + name = "androidx.compose.ui.graphics.Color__Long", + arity = 1, + paramTypes = listOf(TypeTag.INT), + returnType = TypeTag.STRING, + paramNames = listOf("color"), + paramTypeNames = listOf("kotlin.Long"), + returnTypeName = "kotlin.String", + ) { args -> + calls += "Color__Long" + "Long:${(args[0] as Number).toLong()}" + }, // padding, all four overloads, with the receiver in slot 0 exactly as the walker emits it. extension( name = "androidx.compose.foundation.layout.padding__Dp", diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/MethodShapedFragment.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/MethodShapedFragment.kt new file mode 100644 index 000000000..4db8d9a13 --- /dev/null +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/MethodShapedFragment.kt @@ -0,0 +1,149 @@ +package python.multiplatform.ffi.pythonx + +import python.multiplatform.reflection.CallableKind +import python.multiplatform.reflection.ExposedCallable +import python.multiplatform.reflection.FunctionTableFragment +import python.multiplatform.reflection.TypeTag + +/** + * A table shaped the way the artefact walker shapes **member functions** (issue #215), backed by Kotlin + * this repository owns -- the [ComposeShapedFragment] argument for why a stand-in rather than the jars. + * + * | entry | shape it restates | + * |---|---| + * | `methshape.newCounter` | a factory returning a handle, with its ancestry (`Counter<:Base`) | + * | `Counter.add__Int` / `Counter.add__Int_String` | an overloaded member: `METHOD`, receiver in `args[0]`, uncounted in `arity` | + * | `Counter.reset` and the extension `methshape.reset` | a member and an extension of one name on one type: Kotlin calls the member | + * | `Counter.stopNow` | a member whose Pythonic name (`stop_now`) differs | + * | `Base.label` | a member declared only on a supertype | + * | `Counter.count` | a member property, so the count can be read back | + * + * [calls] records every Kotlin-side invocation. + */ +object MethodShapedFragment : FunctionTableFragment { + + override val moduleName: String = "test_method_shaped" + + val calls: MutableList = mutableListOf() + + const val COUNTER = "methshape.Counter" + const val BASE = "methshape.Base" + + override fun entries(): List = listOf( + ExposedCallable( + name = "methshape.newCounter", + arity = 0, + paramTypes = emptyList(), + returnType = TypeTag.OBJECT, + paramNames = emptyList(), + paramTypeNames = emptyList(), + returnTypeName = "$COUNTER<:$BASE", + ) { StubCounter() }, + ExposedCallable( + name = "$COUNTER.add__Int", + arity = 1, + paramTypes = listOf(TypeTag.INT), + returnType = TypeTag.INT, + kind = CallableKind.METHOD, + paramNames = listOf("step"), + paramTypeNames = listOf("kotlin.Int"), + returnTypeName = "kotlin.Int", + receiverTypeName = COUNTER, + paramHasDefault = listOf(false), + ) { args -> + calls += "Counter.add(Int)" + val counter = args[0] as StubCounter + counter.count += (args[1] as Long).toInt() + counter.count.toLong() + }, + ExposedCallable( + name = "$COUNTER.add__Int_String", + arity = 2, + paramTypes = listOf(TypeTag.INT, TypeTag.STRING), + returnType = TypeTag.STRING, + kind = CallableKind.METHOD, + paramNames = listOf("step", "label"), + paramTypeNames = listOf("kotlin.Int", "kotlin.String"), + returnTypeName = "kotlin.String", + receiverTypeName = COUNTER, + paramHasDefault = listOf(false, false), + ) { args -> + calls += "Counter.add(Int, String)" + val counter = args[0] as StubCounter + counter.count += (args[1] as Long).toInt() + (args[2] as String) + counter.count + }, + ExposedCallable( + name = "$COUNTER.reset", + arity = 0, + paramTypes = emptyList(), + returnType = TypeTag.UNIT, + kind = CallableKind.METHOD, + paramNames = emptyList(), + paramTypeNames = emptyList(), + returnTypeName = "kotlin.Unit", + receiverTypeName = COUNTER, + ) { args -> + calls += "Counter.reset" + (args[0] as StubCounter).count = 0 + Unit + }, + ExposedCallable( + name = "methshape.reset", + arity = 1, + paramTypes = listOf(TypeTag.OBJECT), + returnType = TypeTag.UNIT, + paramNames = listOf(""), + paramTypeNames = listOf(COUNTER), + returnTypeName = "kotlin.Unit", + isExtension = true, + receiverTypeName = COUNTER, + paramHasDefault = listOf(false), + ) { _ -> + calls += "extension reset" + Unit + }, + ExposedCallable( + name = "$COUNTER.stopNow", + arity = 0, + paramTypes = emptyList(), + returnType = TypeTag.STRING, + kind = CallableKind.METHOD, + paramNames = emptyList(), + paramTypeNames = emptyList(), + returnTypeName = "kotlin.String", + receiverTypeName = COUNTER, + ) { _ -> + calls += "Counter.stopNow" + "stopped" + }, + ExposedCallable( + name = "$BASE.label", + arity = 0, + paramTypes = emptyList(), + returnType = TypeTag.STRING, + kind = CallableKind.METHOD, + paramNames = emptyList(), + paramTypeNames = emptyList(), + returnTypeName = "kotlin.String", + receiverTypeName = BASE, + ) { _ -> + calls += "Base.label" + "base" + }, + ExposedCallable( + name = "$COUNTER.count", + arity = 0, + paramTypes = emptyList(), + returnType = TypeTag.INT, + kind = CallableKind.GETTER, + paramNames = emptyList(), + paramTypeNames = emptyList(), + returnTypeName = "kotlin.Int", + receiverTypeName = COUNTER, + ) { args -> (args[0] as StubCounter).count.toLong() }, + ) +} + +/** `methshape.Counter`, which is a `methshape.Base`. */ +class StubCounter(var count: Int = 0) diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonicNameShapedFragment.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonicNameShapedFragment.kt new file mode 100644 index 000000000..eaec28342 --- /dev/null +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonicNameShapedFragment.kt @@ -0,0 +1,156 @@ +package python.multiplatform.ffi.pythonx + +import python.multiplatform.reflection.CallableKind +import python.multiplatform.reflection.ExposedCallable +import python.multiplatform.reflection.FunctionTableFragment +import python.multiplatform.reflection.TypeTag + +/** + * A table built to make the Pythonic naming rule (issue #131) **collide**, backed by Kotlin this + * repository owns. + * + * Every entry is here to say one thing about the alias policy in `KotlinSurface.kt` + * (`pythonic_aliases`): an alias is served only when it is unambiguous in its namespace. + * + * | entry | namespace | what it pins | + * |---|---|---| + * | `toURL` / `toUrl` | module | two Kotlin names, one alias (`to_url`): no alias at all, both Kotlin names work | + * | `fooBar` / `foo_bar` | module | the alias of one is the Kotlin name of the other: `foo_bar` is the Kotlin `foo_bar`, never `fooBar` | + * | `rememberTextFieldState` | module | the ordinary case: `remember_text_field_state`, keywords both ways, a Kotlin default | + * | `clicked(onClick, on_click)` | keywords | `onClick`'s alias is the other parameter's Kotlin name: `on_click=` is that parameter | + * | `pair(xURL, xUrl)` | keywords | two parameters, one alias (`x_url`): neither gets it | + * | `box` / [BOX] | proxy | a receiver type for the member namespace | + * | `ext.labelURL` / `ext.labelUrl` on [BOX] | proxy | two members, one alias (`label_url`): none | + * | `ext.fillMax` on [BOX] | proxy | the ordinary member case: `fill_max` | + * | `Box.selectedIndex` (`var`) | proxy | a property read and written through its alias `selected_index` | + * + * [ProxyOnly] is a second fragment for the proxy layer on its own (`PythonProxySource`), with no + * binding layer: module aliases and keyword aliases have to work there too. + */ +object PythonicNameShapedFragment : FunctionTableFragment { + + override val moduleName: String = "test_pythonic_names" + + const val PACKAGE = "pythonic.names" + + const val BOX = "$PACKAGE.Box" + + private const val STRING = "kotlin.String" + + private const val INT = "kotlin.Int" + + override fun entries(): List = listOf( + stringFunction("toURL") { "toURL:" + it }, + stringFunction("toUrl") { "toUrl:" + it }, + ExposedCallable( + name = "$PACKAGE.fooBar", arity = 0, paramTypes = emptyList(), returnType = TypeTag.STRING, + paramNames = emptyList(), paramTypeNames = emptyList(), returnTypeName = STRING, + ) { "fooBar" }, + ExposedCallable( + name = "$PACKAGE.foo_bar", arity = 0, paramTypes = emptyList(), returnType = TypeTag.STRING, + paramNames = emptyList(), paramTypeNames = emptyList(), returnTypeName = STRING, + ) { "foo_bar" }, + rememberTextFieldState("$PACKAGE.rememberTextFieldState"), + ExposedCallable( + name = "$PACKAGE.clicked", arity = 2, + paramTypes = listOf(TypeTag.INT, TypeTag.INT), returnType = TypeTag.STRING, + paramNames = listOf("onClick", "on_click"), paramTypeNames = listOf(INT, INT), returnTypeName = STRING, + ) { args -> "onClick=${args[0]}, on_click=${args[1]}" }, + ExposedCallable( + name = "$PACKAGE.pair", arity = 2, + paramTypes = listOf(TypeTag.INT, TypeTag.INT), returnType = TypeTag.STRING, + paramNames = listOf("xURL", "xUrl"), paramTypeNames = listOf(INT, INT), returnTypeName = STRING, + ) { args -> "xURL=${args[0]}, xUrl=${args[1]}" }, + ExposedCallable( + name = "$PACKAGE.box", arity = 1, + paramTypes = listOf(TypeTag.STRING), returnType = TypeTag.OBJECT, + paramNames = listOf("label"), paramTypeNames = listOf(STRING), returnTypeName = BOX, + ) { args -> PythonicBox(args[0] as String) }, + ExposedCallable( + name = "$PACKAGE.describeBox", arity = 1, + paramTypes = listOf(TypeTag.OBJECT), returnType = TypeTag.STRING, + paramNames = listOf("box"), paramTypeNames = listOf(BOX), returnTypeName = STRING, + ) { args -> (args[0] as PythonicBox).describe() }, + boxExtension("labelURL") { box, _ -> box.plus("labelURL") }, + boxExtension("labelUrl") { box, _ -> box.plus("labelUrl") }, + ExposedCallable( + name = "$PACKAGE.ext.fillMax", arity = 2, + paramTypes = listOf(TypeTag.OBJECT, TypeTag.FLOAT), returnType = TypeTag.OBJECT, + paramNames = listOf("", "maxFraction"), paramTypeNames = listOf(BOX, "kotlin.Float"), + returnTypeName = BOX, isExtension = true, receiverTypeName = BOX, + paramHasDefault = listOf(false, true), + ) { args -> + (args[0] as PythonicBox).plus(if (args[1] == null) "fillMax()" else "fillMax(${args[1]})") + }, + ExposedCallable( + name = "$BOX.selectedIndex", arity = 0, paramTypes = emptyList(), returnType = TypeTag.INT, + kind = CallableKind.GETTER, paramNames = emptyList(), paramTypeNames = emptyList(), + returnTypeName = INT, receiverTypeName = BOX, + ) { args -> + // TypeTag.INT crosses as a Long (the boundary casts the result to it); a Kotlin Int result + // is the fixture's mistake, not the alias path's. + (args[0] as PythonicBox).selectedIndex.toLong() + }, + ExposedCallable( + name = "$BOX.selectedIndex=", arity = 1, paramTypes = listOf(TypeTag.INT), returnType = TypeTag.UNIT, + kind = CallableKind.SETTER, paramNames = listOf("value"), paramTypeNames = listOf(INT), + returnTypeName = "kotlin.Unit", receiverTypeName = BOX, paramHasDefault = listOf(false), + ) { args -> + (args[0] as PythonicBox).selectedIndex = (args[1] as Number).toInt() + Unit + }, + ) + + /** The proxy layer on its own: one package, rendered by `PythonProxySource` with no binding layer. */ + object ProxyOnly : FunctionTableFragment { + + const val PACKAGE = "pythonic.proxyonly" + + override val moduleName: String = "test_pythonic_names_proxy_only" + + override fun entries(): List = listOf( + rememberTextFieldState("$PACKAGE.rememberTextFieldState"), + ExposedCallable( + name = "$PACKAGE.toURL", arity = 1, paramTypes = listOf(TypeTag.STRING), returnType = TypeTag.STRING, + paramNames = listOf("s"), paramTypeNames = listOf(STRING), returnTypeName = STRING, + ) { args -> "toURL:" + args[0] }, + ExposedCallable( + name = "$PACKAGE.toUrl", arity = 1, paramTypes = listOf(TypeTag.STRING), returnType = TypeTag.STRING, + paramNames = listOf("s"), paramTypeNames = listOf(STRING), returnTypeName = STRING, + ) { args -> "toUrl:" + args[0] }, + ) + } + + /** + * `rememberTextFieldState(initialText: String, maxLength: Int = )`. The body is written + * the way a walked defaulted body is (`ArtifactScanner.presenceBranchedCall`): `null` in the + * defaulted slot means "not written", and the label says so. + */ + private fun rememberTextFieldState(name: String) = ExposedCallable( + name = name, arity = 2, + paramTypes = listOf(TypeTag.STRING, TypeTag.INT), returnType = TypeTag.STRING, + paramNames = listOf("initialText", "maxLength"), paramTypeNames = listOf(STRING, INT), + returnTypeName = STRING, paramHasDefault = listOf(false, true), + ) { args -> "state(${args[0]}" + (if (args[1] == null) "" else ", max=${args[1]}") + ")" } + + private fun stringFunction(leaf: String, body: (String) -> String) = ExposedCallable( + name = "$PACKAGE.$leaf", arity = 1, paramTypes = listOf(TypeTag.STRING), returnType = TypeTag.STRING, + paramNames = listOf("s"), paramTypeNames = listOf(STRING), returnTypeName = STRING, + ) { args -> body(args[0] as String) } + + private fun boxExtension(leaf: String, body: (PythonicBox, List) -> PythonicBox) = ExposedCallable( + name = "$PACKAGE.ext.$leaf", arity = 1, paramTypes = listOf(TypeTag.OBJECT), returnType = TypeTag.OBJECT, + paramNames = listOf(""), paramTypeNames = listOf(BOX), returnTypeName = BOX, + isExtension = true, receiverTypeName = BOX, paramHasDefault = listOf(false), + ) { args -> body(args[0] as PythonicBox, args.drop(1)) } +} + +/** What a `pythonic.names.Box` is for [PythonicNameShapedFragment]: a label, a trail, and one `var`. */ +class PythonicBox(private val label: String, private val trail: List = emptyList()) { + + var selectedIndex: Int = 0 + + fun plus(step: String): PythonicBox = PythonicBox(label, trail + step).also { it.selectedIndex = selectedIndex } + + fun describe(): String = (listOf(label) + trail).joinToString(" -> ") + " [selected=$selectedIndex]" +} diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonicNameTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonicNameTest.kt new file mode 100644 index 000000000..86b11979a --- /dev/null +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonicNameTest.kt @@ -0,0 +1,222 @@ +package python.multiplatform.ffi.pythonx + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.PythonTestFixture +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.ffi.upcall.publishesProxyEntryPoints +import python.multiplatform.reflection.HandleTable +import python.multiplatform.reflection.UpcallTable +import python.native.ffi.bindUpcallOrNull +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertTrue + +/** + * Issue #131: on a Kotlin-named module, a declaration, a proxy member and a keyword parameter are + * reachable by their Kotlin names **and** by their Pythonic (snake_case) aliases -- and an alias is + * served only when it is unambiguous ([PythonicNameShapedFragment] is built to collide). + * + * The policy under test (`KotlinSurface.kt`, `pythonic_aliases`): + * + * - an alias that is itself a Kotlin name of the namespace belongs to that Kotlin name; + * - an alias two Kotlin names map to is not served, and both Kotlin names still work; + * - `dir()` and a proxy's class dict list Kotlin names only; + * - `inspect.signature` shows the Pythonic keyword, `describe` both spellings. + * + * Red before #131: every snake_case spelling below is an `AttributeError` or an "unexpected keyword" + * `TypeError`, because `238119b7` removed the conversion. + */ +class PythonicNameTest { + + @AfterTest + fun cleanup() { + UpcallTable.clear() + HandleTable.releaseAll() + } + + /** `remember_text_field_state` is `rememberTextFieldState`; `to_url` is nobody's; `foo_bar` is `foo_bar`'s. */ + @Test + fun aModuleAliasIsServedOnlyWhenItIsUnambiguous() = withBinding { + Python3.exec( + """ + import python_multiplatform as _pn_pm + import pythonic.names as _pn + _pn_r = { + 'same': _pn.remember_text_field_state is _pn.rememberTextFieldState, + 'snake_call': _pn.remember_text_field_state('a', max_length=3), + 'kotlin_call': _pn.rememberTextFieldState('c', maxLength=4), + 'mixed_call': _pn.rememberTextFieldState(initial_text='b'), + 'to_url': hasattr(_pn, 'to_url'), + 'to_urls': [_pn.toURL('x'), _pn.toUrl('x')], + 'foo_bar': _pn.foo_bar(), + 'fooBar': _pn.fooBar(), + 'dir_snake': [_n for _n in dir(_pn) if _n in ('remember_text_field_state', 'describe_box', 'to_url')], + 'described': _pn_pm.describe(_pn, 'remember_text_field_state')['name'], + } + """.trimIndent(), + ) + assertEquals("True", eval("_pn_r['same']"), "an alias must be the Kotlin name's own object") + assertEquals("state(a, max=3)", eval("_pn_r['snake_call']")) + assertEquals("state(c, max=4)", eval("_pn_r['kotlin_call']")) + assertEquals("state(b)", eval("_pn_r['mixed_call']"), "an omitted Kotlin default must stay omitted") + assertEquals("False", eval("_pn_r['to_url']"), "two Kotlin names map to to_url: neither may own it") + assertEquals("['toURL:x', 'toUrl:x']", eval("repr(_pn_r['to_urls'])")) + assertEquals("foo_bar", eval("_pn_r['foo_bar']"), "the Kotlin name foo_bar owns its spelling") + assertEquals("fooBar", eval("_pn_r['fooBar']")) + assertEquals("[]", eval("repr(_pn_r['dir_snake'])"), "dir() lists Kotlin names only") + assertEquals("pythonic.names.rememberTextFieldState", eval("_pn_r['described']")) + } + + /** `on_click=` is the parameter Kotlin calls `on_click`; `x_url=` names neither of two parameters. */ + @Test + fun aKeywordAliasIsServedOnlyWhenItIsUnambiguous() = withBinding { + Python3.exec( + """ + import inspect as _pn_inspect + import python_multiplatform as _pn_pm + import pythonic.names as _pn + _pn_r = { + 'clicked': _pn.clicked(onClick=1, on_click=2), + 'clicked_pos': _pn.clicked(1, on_click=2), + 'clicked_sig': list(_pn_inspect.signature(_pn.clicked).parameters), + 'pair': _pn.pair(xURL=1, xUrl=2), + 'pair_sig': list(_pn_inspect.signature(_pn.pair).parameters), + 'remember_sig': [ + (_p.name, _p.default is _pn_pm.KOTLIN_DEFAULT) + for _p in _pn_inspect.signature(_pn.rememberTextFieldState).parameters.values() + ], + 'remember_described': [ + (_q['name'], _q['python_name']) + for _q in _pn_pm.describe(_pn.rememberTextFieldState)[0]['parameters'] + ], + } + for _pn_label, _pn_call in ( + ('x_url', lambda: _pn.pair(1, x_url=2)), + ('both', lambda: _pn.rememberTextFieldState('a', maxLength=1, max_length=2)), + ): + try: + _pn_call() + _pn_r[_pn_label] = 'accepted' + except TypeError as _pn_e: + _pn_r[_pn_label] = 'TypeError' + """.trimIndent(), + ) + assertEquals("onClick=1, on_click=2", eval("_pn_r['clicked']")) + assertEquals("onClick=1, on_click=2", eval("_pn_r['clicked_pos']")) + assertEquals("['onClick', 'on_click']", eval("repr(_pn_r['clicked_sig'])"), "no alias may shadow a Kotlin name") + assertEquals("xURL=1, xUrl=2", eval("_pn_r['pair']")) + assertEquals("['xURL', 'xUrl']", eval("repr(_pn_r['pair_sig'])")) + assertEquals("TypeError", eval("_pn_r['x_url']"), "x_url would name two parameters") + assertEquals("TypeError", eval("_pn_r['both']"), "two spellings of one parameter are two values") + assertEquals( + "[('initial_text', False), ('max_length', True)]", + eval("repr(_pn_r['remember_sig'])"), + "inspect.signature shows the Pythonic keyword and keeps the Kotlin default", + ) + assertEquals( + "[('initialText', 'initial_text'), ('maxLength', 'max_length')]", + eval("repr(_pn_r['remember_described'])"), + ) + } + + /** `fill_max` is `fillMax`; `label_url` is nobody's; `selected_index` reads and writes the `var`. */ + @Test + fun aMemberAliasIsServedOnlyWhenItIsUnambiguous() = withBinding { + Python3.exec( + """ + import python_multiplatform as _pn_pm + from pythonic.names import box, describe_box + _pn_box = box('b') + _pn_r = { + 'fill': describe_box(_pn_box.fill_max(max_fraction=0.5).fillMax()), + 'label_url': hasattr(_pn_box, 'label_url'), + 'labels': describe_box(_pn_box.labelURL().labelUrl()), + 'member_described': [_d['name'] for _d in _pn_pm.describe_member('pythonic.names.Box', 'fill_max')], + } + _pn_box.selected_index = 4 + _pn_r['read_alias'] = _pn_box.selected_index + _pn_r['read_kotlin'] = _pn_box.selectedIndex + _pn_r['kotlin_side'] = describe_box(_pn_box) + _pn_r['class_dict'] = [_n for _n in ('fill_max', 'selected_index') if _n in vars(type(_pn_box))] + _pn_r['instance_dict'] = [_n for _n in ('selected_index',) if _n in _pn_box.__dict__] + """.trimIndent(), + ) + assertEquals("b -> fillMax(0.5) -> fillMax() [selected=0]", eval("_pn_r['fill']")) + assertEquals("False", eval("_pn_r['label_url']"), "two members map to label_url: neither may own it") + assertEquals("b -> labelURL -> labelUrl [selected=0]", eval("_pn_r['labels']")) + assertEquals("['pythonic.names.ext.fillMax']", eval("repr(_pn_r['member_described'])")) + assertEquals("4", eval("_pn_r['read_alias']")) + assertEquals("4", eval("_pn_r['read_kotlin']")) + assertEquals("b [selected=4]", eval("_pn_r['kotlin_side']"), "the alias write has to reach Kotlin's setter") + assertEquals("[]", eval("repr(_pn_r['class_dict'])"), "an alias is never written onto the proxy class") + assertEquals("[]", eval("repr(_pn_r['instance_dict'])"), "an alias write must not land in the instance dict") + } + + /** + * The proxy layer on its own (`PythonProxySource`, no binding layer): module aliases through the + * PEP 562 `__getattr__` it gives the modules it creates, and keyword aliases through + * `python_multiplatform.kotlin_function`. The collision policy is the same. + */ + @Test + fun theProxyLayerAloneServesTheSameAliases() = PythonTestFixture.withInterpreter { + UpcallTable.install(listOf(PythonicNameShapedFragment.ProxyOnly)) + assertTrue(bindUpcallOrNull("pythonic.proxyonly.toURL"), "the fixture table is not installed") + if (!publishesProxyEntryPoints) { + assertFails { PythonProxySource.install() } + return@withInterpreter + } + Python3.exec( + """ + import sys as _pn_sys + _pn_saved = _pn_sys.modules.pop('python_multiplatform.binding', None) + for _pn_n in [_n for _n in _pn_sys.modules if _n == 'pythonic.proxyonly' or _n.startswith('pythonic.proxyonly.')]: + del _pn_sys.modules[_pn_n] + """.trimIndent(), + ) + try { + PythonProxySource.install() + Python3.exec( + """ + import pythonic.proxyonly as _pn + _pn_r = { + 'same': _pn.remember_text_field_state is _pn.rememberTextFieldState, + 'snake_call': _pn.remember_text_field_state('a', max_length=3), + 'kotlin_kw': _pn.rememberTextFieldState(initialText='b'), + 'to_url': hasattr(_pn, 'to_url'), + 'cached': 'remember_text_field_state' in vars(_pn), + } + """.trimIndent(), + ) + } finally { + Python3.exec( + "if _pn_saved is not None:\n" + + " _pn_sys.modules['python_multiplatform.binding'] = _pn_saved", + ) + } + assertEquals("True", eval("_pn_r['same']")) + assertEquals("state(a, max=3)", eval("_pn_r['snake_call']")) + assertEquals("state(b)", eval("_pn_r['kotlin_kw']")) + assertEquals("False", eval("_pn_r['to_url']")) + assertEquals("False", eval("_pn_r['cached']"), "the proxy layer reads an alias through the Kotlin name every time") + } + + /** Installs [PythonicNameShapedFragment] and the binding layer, as `PythonxAdapterTest.withAdapter` does. */ + private inline fun withBinding(block: () -> Unit) = PythonTestFixture.withInterpreter { + UpcallTable.install(listOf(PythonicNameShapedFragment)) + assertTrue(bindUpcallOrNull("${PythonicNameShapedFragment.PACKAGE}.box"), "the fixture table is not installed") + if (!publishesProxyEntryPoints) { + val refusal = assertFails { PythonxAdapter.install() } + assertTrue( + refusal.message?.contains("raw upcall entry points are not bound") == true, + "a target with no proxy bootstrap must fail the adapter's own guard: $refusal", + ) + return@withInterpreter + } + PythonxAdapter.install() + block() + } + + private fun eval(expression: String): String = PythonTestFixture.eval(expression).toString() +} diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapterTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapterTest.kt index 81fadf8f8..f17a7b0f8 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapterTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxAdapterTest.kt @@ -147,42 +147,111 @@ class PythonxAdapterTest { } /** - * A Kotlin name is reached as itself, and no renamed spelling exists. + * §3's rule, forwards, on the names the fixture carries -- restored by issue #131 after + * `238119b7` removed it. * - * This used to pin a snake_case rule in both directions (`fillMaxWidth` <-> `fill_max_width`) - * and the index that made `toURLString` reachable as `to_url_string`. The binder renames - * nothing now (`docs/INTENT.md` §2.2): Pythonic spellings are the business of a Python package - * built on top, so the rule and its inverse are gone, and asking for the snake_case spelling is - * an ordinary `AttributeError`. + * The rule is pythonx-compose 0.1.0a1's: a lower-case-first name is snake_case with a run of + * capitals as one word, an upper-case-first name is unchanged, and an explicit overload key keeps + * its suffix. There is still no reverse rule: `toURLString` is reachable as `to_url_string` because + * the forward conversion of the module's own Kotlin names is what is looked up, and a reverse rule + * would have produced `toUrlString`, a declaration nobody wrote. */ @Test - fun aKotlinNameIsReachedAsItselfAndNoRenamedSpellingExists() = withAdapter { + fun theNameRuleConvertsKotlinToPythonAndEitherSpellingReachesOneDeclaration() = withAdapter { Python3.exec( """ + import python_multiplatform as _pm import python_multiplatform.binding as _pm_binding import androidx.compose.foundation.layout as _layout import androidx.compose.ui.draw as _draw import androidx.compose.ui.util as _util _px = { - 'kotlin': [ - _n for _m, _n in ( - (_layout, 'fillMaxWidth'), (_draw, 'zIndex'), (_layout, 'padding__Dp_Dp'), - (_util, 'toURLString'), - ) if getattr(_m, _n, None) is not None + 'fill': _pm.python_name('fillMaxWidth'), + 'z': _pm.python_name('zIndex'), + 'overload': _pm.python_name('padding__Dp_Dp'), + 'suffixed': _pm.python_name('paddingFromBaseline__TextUnit'), + 'type': _pm.python_name('Modifier'), + 'acronym': _pm.python_name('toURLString'), + 'keyword': _pm.snake_case('paddingValues'), + 'same': [ + _k for _m, _k, _s in ( + (_layout, 'fillMaxWidth', 'fill_max_width'), (_draw, 'zIndex', 'z_index'), + (_util, 'toURLString', 'to_url_string'), + (_layout, 'paddingFromBaseline__TextUnit', 'padding_from_baseline__TextUnit'), + (_layout, 'paddingValuesOf', 'padding_values_of'), + ) if getattr(_m, _s) is getattr(_m, _k) ], - 'snake': [ - _n for _m, _n in ( - (_layout, 'fill_max_width'), (_draw, 'z_index'), (_util, 'to_url_string'), - ) if hasattr(_m, _n) - ], - 'rule': [_n for _n in ('to_python_name', 'to_kotlin_name') if hasattr(_pm_binding, _n)], + 'url': _util.to_url_string('x'), + 'no_reverse': hasattr(_util, 'toUrlString'), + 'rule_in_binding': [_n for _n in ('to_python_name', 'to_kotlin_name') if hasattr(_pm_binding, _n)], } """.trimIndent(), ) - assertEquals("['fillMaxWidth', 'zIndex', 'padding__Dp_Dp', 'toURLString']", eval("repr(_px['kotlin'])")) - assertEquals("[]", eval("repr(_px['snake'])"), "no snake_case spelling may resolve") - assertEquals("[]", eval("repr(_px['rule'])"), "the binder must carry no renaming rule") + assertEquals("fill_max_width", eval("_px['fill']")) + assertEquals("z_index", eval("_px['z']")) + // The overload suffix is a list of Kotlin type names and stays PascalCase; only the base + // name is a function name. + assertEquals("padding__Dp_Dp", eval("_px['overload']")) + assertEquals("padding_from_baseline__TextUnit", eval("_px['suffixed']")) + assertEquals("Modifier", eval("_px['type']")) + assertEquals("to_url_string", eval("_px['acronym']")) + assertEquals("padding_values", eval("_px['keyword']")) + assertEquals( + "['fillMaxWidth', 'zIndex', 'toURLString', 'paddingFromBaseline__TextUnit', 'paddingValuesOf']", + eval("repr(_px['same'])"), + "an alias must be the very object its Kotlin name is", + ) + assertEquals("url:x", eval("_px['url']")) + assertEquals("False", eval("_px['no_reverse']"), "a reverse rule would invent toUrlString") + // The rule is the root module's, once; the binding layer carries no second copy of it. + assertEquals("[]", eval("repr(_px['rule_in_binding'])")) + } + + /** + * §3's invariant, stated as the design states it -- *every name the `.pyi` generator emits must + * resolve through the adapter* -- restored by issue #131. + * + * The generator writes `python_name(kotlin)` beside every Kotlin name, so this converts every + * entry in the table forwards and requires the module to answer with the very object the Kotlin + * name gives -- including `toURLString`, which a reverse rule provably cannot produce (above). + * Counted, so an empty failure list means something. + */ + @Test + fun everyBoundNameSurvivesTheRoundTripThroughItsPythonicName() = withAdapter { + Python3.exec( + """ + import importlib as _importlib + import python_multiplatform as _pm + import python_multiplatform.binding as _pm_binding + _px = {'checked': 0, 'aliased': 0, 'bad': []} + for _kotlin in _pm_binding.bound_names(): + _package, _, _leaf = _kotlin.rpartition('.') + _python = _pm.python_name(_leaf) + _px['checked'] += 1 + _module = _importlib.import_module(_package) + try: + _by_kotlin = getattr(_module, _leaf) + _by_python = getattr(_module, _python) + except Exception as _e: + _px['bad'].append(_kotlin + ' -> ' + _python + ': ' + type(_e).__name__ + ': ' + str(_e)) + continue + if _python != _leaf: + _px['aliased'] += 1 + # A constant is read afresh on every access, so two reads are two proxies. + if callable(_by_kotlin) and _by_python is not _by_kotlin: + _px['bad'].append(_kotlin + ' -> ' + _python + ': a different object') + _px['bad'] = ', '.join(_px['bad']) + """.trimIndent(), + ) + + assertEquals("", eval("_px['bad']"), "a stub name that the adapter cannot resolve") + assertEquals( + UpcallTable.entries().size.toString(), + eval("_px['checked']"), + "every entry has to be checked, or the empty failure list means nothing", + ) + assertTrue(eval("_px['aliased']").toInt() >= 8, "too few entries had an alias to check: ${eval("_px['aliased']")}") } /** @@ -275,6 +344,78 @@ class PythonxAdapterTest { ) } + /** + * The same dispatch with the **Pythonic** keyword (issue #131, the spelling `238119b7` removed): + * `padding_values=` names only `padding__PaddingValues`'s `paddingValues`, so it is the keyword, + * not the type, that selects -- and the Kotlin keyword `paddingValues=` selects the same overload. + * Writing both spellings of one parameter is "two values", not a silent pick. + */ + @Test + fun anOverloadSetDispatchesOnPythonicKeywordNames() = withAdapter { + Python3.exec( + """ + from androidx.compose.foundation.layout import padding, padding_values_of + from androidx.compose.ui import empty_modifier, describe_modifier + _px = { + 'snake': describe_modifier(padding(empty_modifier(), padding_values=padding_values_of(2))), + 'kotlin': describe_modifier(padding(empty_modifier(), paddingValues=padding_values_of(3))), + } + try: + padding(empty_modifier(), paddingValues=padding_values_of(1), padding_values=padding_values_of(1)) + _px['both'] = 'call succeeded' + except TypeError as e: + _px['both'] = str(e) + """.trimIndent(), + ) + + assertEquals("padding(pv(2.0))", eval("_px['snake']")) + assertEquals("padding(pv(3.0))", eval("_px['kotlin']")) + assertTrue(eval("_px['both']") != "call succeeded", "two spellings of one parameter were both accepted") + assertEquals( + listOf("padding__PaddingValues", "padding__PaddingValues"), + ComposeShapedFragment.calls, + ) + } + + /** + * An int picks the overload its Kotlin literal would (#146, SPEC U-8). `Color(0xFFFFFFFF)` is the + * notebook's own spelling (pythonx-compose `UI.ipynb`); Kotlin types that literal as `Long`, since it + * does not fit in 32 bits, and `0x11223344` as `Int`. An `Int` slot never takes an int outside 32 + * bits, even when named explicitly, and an int outside 64 bits fits no overload at all. + */ + @Test + fun anIntArgumentPicksTheOverloadItsKotlinLiteralWould() = withAdapter { + Python3.exec( + """ + from androidx.compose.ui.graphics import Color, Color__Int, Color__Long + _px = { + 'long': Color(0xFFFFFFFF), + 'int': Color(0x11223344), + 'negative': Color(-1), + 'wide': Color(1 << 40), + 'explicitLong': Color__Long(5), + } + for _key, _call in (('tooWide', lambda: Color(1 << 70)), ('intOverflow', lambda: Color__Int(1 << 31))): + try: + _px[_key] = 'call succeeded: ' + _call() + except TypeError as e: + _px[_key] = 'TypeError' + """.trimIndent(), + ) + + assertEquals("Long:4294967295", eval("_px['long']")) + assertEquals("Int:287454020", eval("_px['int']")) + assertEquals("Int:-1", eval("_px['negative']")) + assertEquals("Long:1099511627776", eval("_px['wide']")) + assertEquals("Long:5", eval("_px['explicitLong']")) + assertEquals("TypeError", eval("_px['tooWide']"), "an int outside 64 bits reached an integral slot") + assertEquals("TypeError", eval("_px['intOverflow']"), "an Int slot took an int outside 32 bits") + assertEquals( + listOf("Color__Long", "Color__Int", "Color__Int", "Color__Long", "Color__Long"), + ComposeShapedFragment.calls, + ) + } + /** * What the dispatcher does when it cannot decide, and what it does when the caller has already * decided. @@ -348,6 +489,38 @@ class PythonxAdapterTest { ) } + /** + * The same chain under the Pythonic method names (issue #131): `fill_max_width` and `z_index` + * are the Kotlin members `fillMaxWidth` and `zIndex`, from the class object and from an instance, + * with a snake_case keyword (`z_index=` is `zIndex`'s own parameter `zIndex`). The proxy class + * keeps Kotlin names only: an alias is resolved on each read and never written onto it. + */ + @Test + fun anExtensionIsReachedOnItsReceiverByItsPythonicNameToo() = withAdapter { + Python3.exec( + """ + from androidx.compose.ui import Modifier, describe_modifier + _from_class = Modifier.fill_max_width().z_index(2) + _from_instance = Modifier.empty().padding(16).fill_max_width().z_index(z_index=3) + _px = { + 'class': describe_modifier(_from_class), + 'instance': describe_modifier(_from_instance), + 'alias_in_class_dict': [_n for _n in ('fill_max_width', 'z_index') if _n in vars(type(_from_class))], + 'alias_in_dir': [_n for _n in ('fill_max_width', 'z_index') if _n in dir(_from_class)], + } + """.trimIndent(), + ) + + assertEquals("fillMaxWidth -> zIndex(2.0)", eval("_px['class']")) + assertEquals("padding(16.0) -> fillMaxWidth -> zIndex(3.0)", eval("_px['instance']")) + assertEquals("[]", eval("repr(_px['alias_in_class_dict'])")) + assertEquals("[]", eval("repr(_px['alias_in_dir'])")) + assertEquals( + listOf("fillMaxWidth", "zIndex", "padding__Dp", "fillMaxWidth", "zIndex"), + ComposeShapedFragment.calls, + ) + } + /** * The negative half of the test above, and the reason to trust it. * diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxCallableTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxCallableTest.kt index 7c7492c30..8140363d7 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxCallableTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxCallableTest.kt @@ -29,7 +29,7 @@ import kotlin.test.assertTrue * | | where | * |---|---| * | does a Python callable become a Kotlin `FunctionN`, with the composer threaded through | here, on all five targets, against [ComposableShapedFragment] | - * | does a real `androidx.compose.foundation.layout.Column` then draw its content | `:ksp-fixtures:compose`'s `ComposableRenderTest`, desktop only | + * | does a real `androidx.compose.foundation.layout.Column` then draw its content | `:python-multiplatform-compose:fixtures:compose`'s `ComposableRenderTest`, desktop only | * | does Compose give the callable back when it drops the slot | `ComposableRenderTest.aDisposedCompositionGivesEveryPythonCallableBack`, because `RememberObserver` is a Compose type | * * The split is [ComposableShapedFragment]'s: `commonTest` runs where there is no Compose runtime, no @@ -254,7 +254,7 @@ class PythonxCallableTest { * * This used to assert the opposite, and the opposite was true: one wrapper per crossing, nothing * interned, so *n* crossings cost *n* Python references and *n* `HandleTable` roots held until - * the scope closed. `:ksp-fixtures:compose`'s `RecompositionAccumulationTest` measured what that + * the scope closed. `:python-multiplatform-compose:fixtures:compose`'s `RecompositionAccumulationTest` measured what that * came to for a real composition -- **twelve** wrappers for one `content=` composed twelve times, * and twenty-four for a `Button` -- and `PythonCallableScope` now keys them. The number here is 1. * @@ -363,7 +363,7 @@ class PythonxCallableTest { """ from androidx.compose.material3 import Slider _px_seen = [] - Slider(onValueChange=lambda v: _px_seen.append(v)) + Slider(on_value_change=lambda v: _px_seen.append(v)) """.trimIndent(), ) @@ -597,8 +597,8 @@ class PythonxCallableTest { val refusal = assertFails { Python3.exec( """ - from androidx.compose.material3 import rememberSheetState - rememberSheetState(confirmValueChange=lambda v: True) + from androidx.compose.material3 import remember_sheet_state + remember_sheet_state(confirm_value_change=lambda v: True) """.trimIndent(), ) } diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxComposableTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxComposableTest.kt index 2ff8dddee..1a9f7ab68 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxComposableTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxComposableTest.kt @@ -40,7 +40,7 @@ import kotlin.test.assertTrue * * It is not a proof that Compose draws anything. The fragment behind it is Kotlin this repository * owns ([ComposableShapedFragment]), because `commonTest` runs on Kotlin/Native where there is no - * Compose runtime and no generated thunk class. `:ksp-fixtures:compose`'s `ComposableRenderTest` is + * Compose runtime and no generated thunk class. `:python-multiplatform-compose:fixtures:compose`'s `ComposableRenderTest` is * the other half, and it renders a real `Text` through a real `Composer`. */ class PythonxComposableTest { @@ -102,7 +102,7 @@ class PythonxComposableTest { Text('hi') Text('hi', modifier=None) Text('hi', color=3) - Text('hi', fontSize=4) + Text('hi', font_size=4) Text('hi', color=3, fontSize=4) """.trimIndent(), ) @@ -149,7 +149,7 @@ class PythonxComposableTest { """ from androidx.compose.foundation.layout import stubRowScope _row = stubRowScope() - _row.NavigationBarItem(selected=True, onClick=lambda: None) + _row.NavigationBarItem(selected=True, on_click=lambda: None) _row.NavigationBarItem(selected=True, onClick=lambda: None, enabled=False) """.trimIndent(), ) diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxDefaultsTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxDefaultsTest.kt index 80c0c550d..578d6ea87 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxDefaultsTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxDefaultsTest.kt @@ -72,7 +72,7 @@ class PythonxDefaultsTest { """ from androidx.compose.ui import Modifier, describeModifier _px = { - 'omitted': describeModifier(Modifier.fillMaxHeight()), + 'omitted': describeModifier(Modifier.fill_max_height()), 'given': describeModifier(Modifier.fillMaxHeight(0.5)), } """.trimIndent(), diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxMethodTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxMethodTest.kt new file mode 100644 index 000000000..e4b767bba --- /dev/null +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/pythonx/PythonxMethodTest.kt @@ -0,0 +1,134 @@ +package python.multiplatform.ffi.pythonx + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.PythonTestFixture +import python.multiplatform.ffi.upcall.publishesProxyEntryPoints +import python.multiplatform.reflection.HandleTable +import python.multiplatform.reflection.UpcallTable +import python.native.ffi.bindUpcallOrNull +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertTrue + +/** + * Issue #215's binding-layer half: a `METHOD` row the walker emits is a method of the proxy of the + * type it is declared on, reached on that type and its subtypes, ahead of an extension of the same + * name, and never a module attribute. + * + * The table is [MethodShapedFragment]. + * + * ### Red before the implementation, and what each red says + * + * Against a binding layer that indexes every non-property row by package, `counter.add(3)` is an + * `AttributeError` ("no Kotlin extension named add"), `counter.reset()` calls the extension, and + * `methshape.Counter` is importable as a package of its own. A *regression* reads differently: a + * value that comes back wrong, or the wrong Kotlin call recorded. + */ +class PythonxMethodTest { + + @BeforeTest + fun install() { + UpcallTable.install(listOf(ComposeShapedFragment, MethodShapedFragment)) + MethodShapedFragment.calls.clear() + } + + @AfterTest + fun cleanup() { + UpcallTable.clear() + HandleTable.releaseAll() + } + + @Test + fun aMemberIsCalledOnTheProxyWithItsReceiver() = withAdapter { + Python3.exec( + """ + from methshape import newCounter + _pm_c = newCounter() + _pm = {'first': _pm_c.add(3), 'keyword': _pm_c.add(step=2), 'count': _pm_c.count} + """.trimIndent(), + ) + assertEquals("3", eval("_pm['first']")) + assertEquals("5", eval("_pm['keyword']")) + assertEquals("5", eval("_pm['count']"), "the member did not run on the Kotlin object the proxy holds") + assertEquals(listOf("Counter.add(Int)", "Counter.add(Int)"), MethodShapedFragment.calls) + } + + @Test + fun anOverloadedMemberIsChosenByItsArgumentsAndByItsExplicitName() = withAdapter { + Python3.exec( + """ + from methshape import newCounter + _pm_c = newCounter() + _pm = {'two': _pm_c.add(1, 'n='), 'explicit': _pm_c.add__Int(4)} + """.trimIndent(), + ) + assertEquals("n=1", eval("_pm['two']")) + assertEquals("5", eval("_pm['explicit']")) + assertEquals(listOf("Counter.add(Int, String)", "Counter.add(Int)"), MethodShapedFragment.calls) + } + + @Test + fun aMemberWinsOverAnExtensionOfTheSameName() = withAdapter { + Python3.exec( + """ + from methshape import newCounter + _pm_c = newCounter() + _pm_c.add(7) + _pm_c.reset() + _pm = {'count': _pm_c.count} + """.trimIndent(), + ) + assertEquals("0", eval("_pm['count']")) + assertEquals(listOf("Counter.add(Int)", "Counter.reset"), MethodShapedFragment.calls) + } + + @Test + fun aSupertypesMemberIsReachedOnTheSubtype() = withAdapter { + Python3.exec("from methshape import newCounter\n_pm = {'label': newCounter().label()}") + assertEquals("base", eval("_pm['label']")) + assertEquals(listOf("Base.label"), MethodShapedFragment.calls) + } + + @Test + fun aMembersPythonicNameReachesIt() = withAdapter { + Python3.exec("from methshape import newCounter\n_pm = {'snake': newCounter().stop_now(), 'kotlin': newCounter().stopNow()}") + assertEquals("stopped", eval("_pm['snake']")) + assertEquals("stopped", eval("_pm['kotlin']")) + } + + @Test + fun aMemberIsNotAModuleAttribute() = withAdapter { + Python3.exec( + """ + import importlib, importlib.util, methshape + _pm = { + 'add_in_package': hasattr(methshape, 'add__Int'), + 'counter_is_a_package': importlib.util.find_spec('methshape.Counter') is not None, + } + """.trimIndent(), + ) + assertEquals("False", eval("_pm['add_in_package']")) + assertEquals("False", eval("_pm['counter_is_a_package']")) + } + + // ------------------------------------------------------------------------------- helpers + + private inline fun withAdapter(block: () -> Unit) = PythonTestFixture.withInterpreter { + assertTrue(bindUpcallOrNull(ComposeShapedFragment.EMPTY_MODIFIER), "the fixture table is not installed") + if (!publishesProxyEntryPoints) { + val refusal = assertFails { PythonxAdapter.install(COMPOSE_SHAPED_RAW_VALUE_CLASSES) } + assertTrue( + refusal.message?.contains("raw upcall entry points are not bound") == true, + "a target with no proxy bootstrap must fail the adapter's own guard: $refusal", + ) + return@withInterpreter + } + PythonxAdapter.install(COMPOSE_SHAPED_RAW_VALUE_CLASSES) + block() + } + + private fun eval(expression: String): String = PythonTestFixture.eval(expression).toString() +} diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/PythonProxySourceTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/PythonProxySourceTest.kt index bb22a1dce..848bf40b4 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/PythonProxySourceTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/PythonProxySourceTest.kt @@ -380,10 +380,26 @@ class PythonProxySourceTest { // 551-587 ns per read on desktop against 6-14 ns for the descriptor that replaced it. A // hook reintroduced here would be a fifty-fold regression on every top-level `val` read, // and it would be invisible -- both shapes are correct. + // + // Issue #131 adds one hook, and it is the exception that keeps the rule: the PEP 562 + // `__getattr__` of `KotlinSurface.module_alias_getattr` runs only for a name the module does + // NOT have (a Pythonic alias), so a real Kotlin name is still found by the descriptor first + // and never reaches it. It lives in the delivered `KotlinSurface` half; the proxy half, which + // renders every real attribute, must define no hook of its own. + val proxyHalf = PythonProxySource.support.removePrefix(python.multiplatform.ffi.upcall.KotlinSurface.DELIVERY) assertFalse( - PythonProxySource.support.contains("def __getattr__("), + proxyHalf.contains("def __getattr__("), "a module attribute must be answered by a data descriptor, not by the fallback hook", ) + assertEquals( + 1, + Regex("def __getattr__\\(").findAll(python.multiplatform.ffi.upcall.KotlinSurface.DELIVERY).count(), + "the only fallback hook is the alias one in module_alias_getattr", + ) + assertTrue( + python.multiplatform.ffi.upcall.KotlinSurface.DELIVERY.contains("def module_alias_getattr(module):"), + "the single hook must be the alias hook", + ) assertFalse( PythonProxySource.support.contains("def __setattr__("), "a data descriptor answers the write half too; a __setattr__ hook means one is missing", @@ -826,7 +842,7 @@ class PythonProxySourceTest { // A rendered class must be a `_PmObject` (so `_pm_unwrap` accepts an instance of one) *and* // its statics must sit on a metaclass. Written the obvious way those two read as // `class Foo(_PmObject, metaclass=_pm_t_1):`, and that spelling is what broke - // `ksp-fixtures:app`: see [theCompanionShapeAConsumerMatchesWithALazyRegexStaysAdjacent]. + // `python-multiplatform-ksp:fixtures:app`: see [theCompanionShapeAConsumerMatchesWithALazyRegexStaysAdjacent]. // // Python has no syntax for putting a base *after* a keyword, so the base has to come from // somewhere other than the base list, and the metaclass is the only thing left holding the @@ -860,7 +876,7 @@ class PythonProxySourceTest { @Test fun theCompanionShapeAConsumerMatchesWithALazyRegexStaysAdjacent() { // Pinned here because the consumer that reads it cannot pin it for itself, and because the - // way it failed hid what had happened. `ksp-fixtures:app`'s + // way it failed hid what had happened. `python-multiplatform-ksp:fixtures:app`'s // `GeneratedStaticPropertyProxyTest` locates a companion's metaclass with // // class (_pm_t_\d+)\(type\):\n(?:.|\n)*?class Foo\(metaclass=\1\): diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/UpcallBootstrapTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/UpcallBootstrapTest.kt index 0d20f9a4b..6171499d4 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/UpcallBootstrapTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/upcall/UpcallBootstrapTest.kt @@ -11,7 +11,7 @@ import kotlin.test.assertTrue * existed, a consumer outside `python-multiplatform` had no route to the four names * [PythonProxySource.install]'s entry-point guard requires -- only this module's own **test** * source (`UpcallEntryBridge.desktop.kt`) rebuilt them, by hand, from the public - * `python.native.ffi.UpcallStub` addresses. `ksp-fixtures/artifact`'s + * `python.native.ffi.UpcallStub` addresses. `python-multiplatform-gradle-plugin/fixtures/artifact`'s * `WalkedArtifactPythonImportTest` copied that workaround because there was nothing else to call; * it now calls this instead. */ diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/DoubleReleaseTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/DoubleReleaseTest.kt index 9ddbdba6f..892842e94 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/DoubleReleaseTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/DoubleReleaseTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertTrue * `GCLeakTest` asks whether a dropped wrapper's reference eventually comes back, and every one of * its assertions is a *lower* bound: the count fell, a cleaner ran, the delta is positive. A * mechanism that gives the same reference back **twice** satisfies all of them. That is not a - * hypothetical failure in this repository — a wrapper taking ownership of a pointer it had only + * hypothetical failure in this repository, a wrapper taking ownership of a pointer it had only * borrowed produced exactly one extra release per wrapper, corrupted CPython's free lists, and * surfaced as a segfault in a test that had nothing to do with the code at fault. It is the reason * ROADMAP §1 looked unfixable for three attempts. @@ -27,7 +27,7 @@ import kotlin.test.assertTrue * The target carries [BALLAST] extra references throughout. Without them, a double free of * [WRAPPERS] references would drive the count to zero, CPython would free an object this test is * still holding, and the failure would arrive as a crash somewhere else instead of as a readable - * assertion here — which is precisely the failure mode being guarded against, and a test that + * assertion here, which is precisely the failure mode being guarded against, and a test that * reproduces it uncontrolled is worth less than one that reports it. */ private const val WRAPPERS = 100 @@ -102,8 +102,8 @@ private fun takeAndDrop(target: PyObject, count: Int) { /** * A cross-thread counter for release actions. * - * `@Volatile` rather than an atomic because each instance is written by exactly one releaser — the - * cleaner thread for a dropped cleaner, the test thread for an explicit `close()` — so there is no + * `@Volatile` rather than an atomic because each instance is written by exactly one releaser, the + * cleaner thread for a dropped cleaner, the test thread for an explicit `close()`, so there is no * read-modify-write to lose. Two counters, never one shared between two cleaners, for that reason. */ private class ReleaseCount { diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/EvalCheckpointTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/EvalCheckpointTest.kt index b4e3df7f7..3ef23f66a 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/EvalCheckpointTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ref/EvalCheckpointTest.kt @@ -15,7 +15,7 @@ import kotlin.time.measureTime /** * What an embedder owes CPython that a script never has to think about (ROADMAP §9). * - * CPython defers work to a checkpoint that only the evaluation loop reaches — `_Py_HandlePending`, + * CPython defers work to a checkpoint that only the evaluation loop reaches, `_Py_HandlePending`, * called from the `_CHECK_PERIODIC` uop that opens every Python-level frame. Nothing in the C API * reaches it on its own. A program that drives the interpreter entirely through `PyObject_Call` * and friends therefore never merges the free-threaded build's biased reference-counting queue, @@ -296,7 +296,7 @@ class EvalCheckpointTest { /** * A checkpoint must be cheap enough to ride on ordinary calls, which is the whole reason it is - * a cached function object and not `exec("pass")` — the latter recompiles a module every time. + * a cached function object and not `exec("pass")`, the latter recompiles a module every time. * * The assertion is the ordering, not an absolute number: absolute nanoseconds are a property * of the machine, but "a cached call beats recompiling" is a property of the design and would @@ -343,9 +343,9 @@ class EvalCheckpointTest { } /** - * The automatic path must not fire from a cleaner. It is suppressed there for two reasons — + * The automatic path must not fire from a cleaner. It is suppressed there for two reasons, * the queue that needs merging belongs to the owning thread, and running Python on a cleaner is - * what ROADMAP §1 turned into a deadlock — and neither is visible from the outside, so it is + * what ROADMAP §1 turned into a deadlock, and neither is visible from the outside, so it is * pinned here by counting checkpoints while only cleaners are running. */ @Test diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/reflection/TestFragments.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/reflection/TestFragments.kt index 4ac437839..d49da0286 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/reflection/TestFragments.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/reflection/TestFragments.kt @@ -174,7 +174,7 @@ enum class TestColor(val weight: Long) { /** * The declaration kinds whose members do not take a receiver -- companion/object members and * enum entries -- plus the class descriptors that tell the Python side what shape to build. - * Written the way `python-multiplatform-ksp` emits them; `ksp-fixtures` proves the generator + * Written the way `python-multiplatform-ksp` emits them; `python-multiplatform-ksp/fixtures` proves the generator * actually does. */ object TestStaticsFragment : FunctionTableFragment { diff --git a/python-multiplatform/src/commonTest/kotlin/python/native/ffi/EmbedApiLowLevelTest.kt b/python-multiplatform/src/commonTest/kotlin/python/native/ffi/EmbedApiLowLevelTest.kt index 93735a4df..b0051d7e0 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/native/ffi/EmbedApiLowLevelTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/native/ffi/EmbedApiLowLevelTest.kt @@ -14,7 +14,7 @@ import python.multiplatform.ffi.PythonTestFixture * * ### Why this file exists * - * Every other test under `commonTest` exercises the assembled layer — `Python3`, `PyObject`, + * Every other test under `commonTest` exercises the assembled layer, `Python3`, `PyObject`, * the collections. That is the right default, but it means a broken individual binding is only * ever seen through whatever the object model happens to call, and only in the combinations it * happens to use. Android demonstrated the cost of the mirror-image gap: its instrumented tests @@ -26,7 +26,7 @@ import python.multiplatform.ffi.PythonTestFixture * where it found it. * * The reference conventions are the part worth being explicit about, because getting one wrong - * is silent in both directions — one too few leaks, one too many frees an object still in use + * is silent in both directions, one too few leaks, one too many frees an object still in use * and the crash lands somewhere unrelated. */ class EmbedApiLowLevelTest { diff --git a/python-multiplatform/src/desktopMain/README.md b/python-multiplatform/src/desktopMain/README.md index a09a2b612..a8bd1e185 100644 --- a/python-multiplatform/src/desktopMain/README.md +++ b/python-multiplatform/src/desktopMain/README.md @@ -1,4 +1,4 @@ -# desktopMain — rules +# desktopMain, rules JVM on macOS, Linux and Windows, reaching CPython through Project Panama. @@ -11,7 +11,7 @@ JVM on macOS, Linux and Windows, reaching CPython through Project Panama. inline fun PyList_Size(list: Long): Long = PyList_SizeHandle.invokeExact(list) as Long This is not a micro-optimisation. Converting the bulk of these wrappers took a real C API call -(`PyList_Size` on `sys.path`) from **1015.95 ns to 2.65 ns** — about 380x. Before that, desktop +(`PyList_Size` on `sys.path`) from **1015.95 ns to 2.65 ns**, about 380x. Before that, desktop was the slowest FFI path in the project by two orders of magnitude, on the machine that is fastest at everything else. @@ -23,7 +23,7 @@ proves nothing; the tests are the check. Both are 8 bytes and travel in the same register on a 64-bit ABI. `ADDRESS` makes Panama hand back a `MemorySegment`, which then has to be unwrapped through per-call argument and return -filters — and those filters were reflective `MethodHandle` invocations building a segment on +filters, and those filters were reflective `MethodHandle` invocations building a segment on every call. `JAVA_LONG` deletes them, and it is what makes the handle type `(long, long, ...) -> long` so that `invokeExact` is reachable at all. @@ -31,14 +31,14 @@ every call. `JAVA_LONG` deletes them, and it is what makes the handle type The backend resolves `java.lang.foreign` or `jdk.incubator.foreign` reflectively so the code compiles against any JDK without importing either. That is correct and should stay. What must -never happen is reflection reaching the per-call path — that was the other half of the 1015 ns. +never happen is reflection reaching the per-call path, that was the other half of the 1015 ns. Resolve handles once per symbol at startup; after that a call is a handle invocation and nothing else. ## String ownership `withUtf8 { }` allocates, passes the address, and frees in a `finally`. Values owned by CPython -(`Py_GetVersion`, `PyUnicode_AsUTF8`) are read and **not** freed — freeing them is a +(`Py_GetVersion`, `PyUnicode_AsUTF8`) are read and **not** freed, freeing them is a use-after-free. Desktop's string marshalling costs about 200 ns per string, measured at 606.74 ns (10.5%) of a @@ -49,7 +49,7 @@ off-heap without crossing the boundary at all. Composing binder operations natively was measured and rejected for desktop: it would buy ~10% and cost macOS/Linux Kotlin/Native targets, a packaging path, and permanent Windows asymmetry. -See ROADMAP §6. Android is different — there the same measurement gives 1.8x. +See ROADMAP §6. Android is different, there the same measurement gives 1.8x. ## Never hand-edit `reachability-metadata.json` @@ -59,7 +59,7 @@ the non-derivable half (`reflection`, and any `resources` globs) is checked in, `python-multiplatform/native-image/reachability-metadata.base.json`. Under GraalVM's closed world, a `FunctionDescriptor` that is not declared there does not fail the -build — it fails on the first call, with `MissingForeignRegistrationError`. So every new descriptor +build, it fails on the first call, with `MissingForeignRegistrationError`. So every new descriptor has to reach the metadata by construction: - A new CPython entry point is a `find("sym", ReturnType, Params...)` line in `bindings.kt`, and @@ -69,14 +69,14 @@ has to reach the metadata by construction: parameterTypes = [...])` marker directly above it; its descriptor is assembled reflectively and cannot be read out of the code. The generator refuses to run without the marker. -`ReachabilityMetadataTest` checks the shipped file from the other direction — against the -`MethodHandle.type()`s the loaded classes actually built — so a parser that stops recognising a +`ReachabilityMetadataTest` checks the shipped file from the other direction, against the +`MethodHandle.type()`s the loaded classes actually built, so a parser that stops recognising a declaration form surfaces as a failing test, not as a binary that dies at a customer. ## The stdlib is staged by the Gradle plugin, and `PYTHONHOME` is set when the JVM starts `desktopJar` carries `libpython` for four platforms under `lib//` and **no standard -library** — verified, not assumed: the published jar's `lib/` tree is 14 entries, all of them +library**, verified, not assumed: the published jar's `lib/` tree is 14 entries, all of them shared libraries. `Py_Initialize()` therefore dies with `Failed to import encodings module` until `PYTHONHOME` names a prefix that has one. @@ -86,13 +86,13 @@ cannot set an environment variable for itself.** CPython reads `PYTHONHOME` with `System.getenv` is an immutable snapshot taken at start-up, and mutating it (reflectively or otherwise) does not touch the native environment CPython reads. Android's `PythonBootstrap` calls `Os.setenv`; there is no such call here. A runtime helper would have to reach libc `setenv` -through Panama — a different symbol on Windows (`_putenv_s`) — and would then be setting a value +through Panama, a different symbol on Windows (`_putenv_s`), and would then be setting a value `PythonHomeCheck` could no longer read. Setting it as the child process is launched, which is what Gradle's `environment(...)` does and what this repo's own `desktopTest` already does, leaves CPython and `PythonHomeCheck` reading the same value from the same place. The staged prefix is shared per machine (under the Gradle user home), keyed by version + upstream -release + platform, and stamped after the last extracted byte — so an interrupted extraction is +release + platform, and stamped after the last extracted byte, so an interrupted extraction is never mistaken for a finished one. See ROADMAP §15h. ## A packaged app names its prefix in-process, with `Py_SetPythonHome` (SPEC L-9) @@ -115,7 +115,7 @@ The section above is about `PYTHONHOME` **the environment variable**. A packaged `Py_SetPythonHome` is deprecated since 3.11 in favour of `PyConfig.home`, but `PyConfig` is a struct whose layout the Stable ABI does not promise, and the function is in the Stable ABI, so its -symbol stays exported. It takes `wchar_t *` — UTF-32 here, UTF-16 on Windows — encoded by hand in +symbol stays exported. It takes `wchar_t *`, UTF-32 here, UTF-16 on Windows, encoded by hand in `encodeWideString` rather than through `Py_DecodeLocale`, which before pre-initialisation decodes with the C locale (the ANSI code page on Windows). The buffer is never freed (a few hundred bytes, once per process): the API asks for static storage. @@ -152,11 +152,11 @@ same completion-marker discipline `PythonBootstrap.stageStdlib` uses, and the re **Do not match on the name `python/`.** This library's own top-level Kotlin package *is* `python`, so `python/multiplatform` and `python/native` class files answer `getResources("python/")` on every -classpath this library is on — the build output directory during a Gradle build, and +classpath this library is on, the build output directory during a Gradle build, and `python-multiplatform.jar` itself once published. Name-based discovery therefore puts a directory of `.class` files at `sys.path[0]` in every consuming application, and extracts the whole library jar to do it. Observed, not theorised: the first run of `PythonPayloadTest` returned -`build/classes/kotlin/desktop/{main,test}/python`. Discovery asks what is *in* the root instead — at +`build/classes/kotlin/desktop/{main,test}/python`. Discovery asks what is *in* the root instead, at least one immediate child that is a `.py`/`.pyc`, or a directory with an `__init__` in it, which is exactly what `ResourceBundler` produces. @@ -175,6 +175,6 @@ does not exist until `Py_Initialize()` has built it. resources into the image heap: that copy was 19.4 MB of a 35.4 MB binary, written straight back out to a temp file at startup. The library resource is therefore *not* registered, and `manager.kt` falls back to `$PYTHONHOME/lib/` (`PYTHON_MULTIPLATFORM_LIBPYTHON` overrides an absolute path). -`PYTHONHOME` has to point at a real prefix regardless — `Py_Initialize` needs the matching stdlib — +`PYTHONHOME` has to point at a real prefix regardless, `Py_Initialize` needs the matching stdlib, so nothing is lost, and the binary is 16.7 MB. Loading the library out of the same prefix as the stdlib also removes the version-skew hazard the jar copy had. diff --git a/python-multiplatform/src/desktopMain/kotlin/python/multiplatform/env/PythonPayload.desktop.kt b/python-multiplatform/src/desktopMain/kotlin/python/multiplatform/env/PythonPayload.desktop.kt index 24a4a2ac0..0b9f897d2 100644 --- a/python-multiplatform/src/desktopMain/kotlin/python/multiplatform/env/PythonPayload.desktop.kt +++ b/python-multiplatform/src/desktopMain/kotlin/python/multiplatform/env/PythonPayload.desktop.kt @@ -15,7 +15,7 @@ import java.util.zip.ZipEntry * * `toolchain`'s `stagePythonBundleDesktop` registers `build/pythonStaging/desktop` as a JVM * resource root, so `processResources` copies `python/...` to the root of the consumer's jar. That - * is a **classpath resource, not a filesystem path** — and CPython's importer opens modules with + * is a **classpath resource, not a filesystem path**, and CPython's importer opens modules with * `open(2)`, which cannot see inside a zip. So a jar payload has to be materialised before it can * go on `sys.path`; a payload sitting on the classpath as a plain directory (a Gradle `run`, an * exploded application image, an IDE run configuration) already is one and is used where it lies. @@ -42,7 +42,7 @@ internal object ClasspathPayload { * * The context class loader is what an application server, a Gradle test worker or any other * container sets to "the class path of the thing currently running", and it is the only one a - * test can substitute — which is what makes the start-up path testable at all rather than only + * test can substitute, which is what makes the start-up path testable at all rather than only * its inner half. This class's own loader is the fallback and covers the ordinary case where * the library and the payload ship on the same class path. */ @@ -55,7 +55,7 @@ internal object ClasspathPayload { * `/python-multiplatform-payload`, overridable with the `python.multiplatform.payload.cache` * system property. * - * The temporary directory rather than a user cache directory because the content is derived — + * The temporary directory rather than a user cache directory because the content is derived, * losing it costs one re-extraction, and the stamp key makes a stale copy impossible rather * than merely unlikely. */ @@ -113,7 +113,7 @@ internal object ClasspathPayload { * `python/` as the class loader sees it, asked for both with and without the trailing slash. * * A jar can only answer either of these from a `python/` **directory entry**, and not every - * archive has one — `JarOutputStream` writes directory entries only if asked. [classPathJarFiles] + * archive has one, `JarOutputStream` writes directory entries only if asked. [classPathJarFiles] * is the fallback for that case; asking here first is what keeps the common case a single * lookup instead of a class path scan. */ @@ -145,14 +145,14 @@ internal object ClasspathPayload { * collision nobody had noticed: this library's own top-level Kotlin package is `python`.** * `python.multiplatform` and `python.native` compile to class files under `python/multiplatform` * and `python/native`, so *every* class path this library is on already answers - * `getResources("python/")` — with the library's own class output directory during a Gradle + * `getResources("python/")`, with the library's own class output directory during a Gradle * build, and with a `jar:` URL into `python-multiplatform.jar` once it is published. Name-based * discovery would therefore have put a directory of `.class` files at `sys.path[0]` in every * application that used this library, and extracted the entire library jar to do it. Observed, * not theorised: this returned * `build/classes/kotlin/desktop/{main,test}/python` on the very first run of the suite. * - * The test is what `ResourceBundler` itself produces — its `findPythonPackages` bundles exactly + * The test is what `ResourceBundler` itself produces, its `findPythonPackages` bundles exactly * those immediate children that are directories holding an `__init__.py`, plus whatever the * `src/` overlay adds. So: at least one immediate child that is a module file, or a * directory with an `__init__` in it. A `python/` holding only `.class` files fails it, and so @@ -185,7 +185,7 @@ internal object ClasspathPayload { private fun String.isModuleFileName(): Boolean = endsWith(".py") || endsWith(".pyc") - /** `bytecode`-level bundles ship `.pyc` beside (or instead of) the `.py` — see `ResourceBundler` assumption 6. */ + /** `bytecode`-level bundles ship `.pyc` beside (or instead of) the `.py`, see `ResourceBundler` assumption 6. */ private val INIT_NAMES = listOf("__init__.py", "__init__.pyc") private fun jarOf(url: URL): JarFile? = runCatching { @@ -195,8 +195,8 @@ internal object ClasspathPayload { /** * The jars on [loader]'s own class path, for the archives [payloadUrls] cannot see into. * - * `URLClassLoader` is asked directly; for everything else — including the `AppClassLoader` a - * plain `java -jar` run gets — `java.class.path` is the only description of the class path the + * `URLClassLoader` is asked directly; for everything else, including the `AppClassLoader` a + * plain `java -jar` run gets, `java.class.path` is the only description of the class path the * JVM exposes. Both are walked up the parent chain, because a payload jar is as likely to be * on a parent loader as on the one that loaded this class. */ @@ -233,8 +233,8 @@ internal object ClasspathPayload { * Materialises the `python/` subtree of [jar] under [cacheDir], or returns null when it holds * none. * - * The directory is named by a digest of *what is in the archive* — every payload entry's name, - * size and CRC — rather than by the jar's path or timestamp. Two consequences, both wanted: + * The directory is named by a digest of *what is in the archive*, every payload entry's name, + * size and CRC, rather than by the jar's path or timestamp. Two consequences, both wanted: * a rebuilt jar with identical content reuses the extraction, and a jar rewritten in place with * different content cannot be mistaken for it. The CRCs come from the archive's central * directory, so the key costs no reading of file content at all. @@ -254,7 +254,7 @@ internal object ClasspathPayload { if (stamp.isFile && stamp.readText() == key) return root stamp.delete() - root.deleteRecursively() + deleteWithoutFollowingLinks(root) if (!root.mkdirs() && !root.isDirectory) { throw IOException("could not create $root to extract the Python payload from ${archive.name} into") } diff --git a/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/Panama.kt b/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/Panama.kt index d16c8a634..21054441e 100644 --- a/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/Panama.kt +++ b/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/Panama.kt @@ -25,7 +25,7 @@ internal object Panama { // ---- public surface used by bindings.kt ---- - /** Pointer type carrier for MethodType signatures — always Long. */ + /** Pointer type carrier for MethodType signatures, always Long. */ val POINTER_TYPE: Class<*> = Long::class.javaPrimitiveType!! /** The null-pointer sentinel as a Long. */ @@ -649,7 +649,7 @@ internal object Panama { return h } - // Static adapters called via MethodHandle — must use Any to avoid importing MemorySegment + // Static adapters called via MethodHandle, must use Any to avoid importing MemorySegment @JvmStatic fun modernSegmentToLong(seg: Any?): Long { if (seg == null) return 0L diff --git a/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/bindings.kt b/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/bindings.kt index 62030d26e..e09e7c1a6 100644 --- a/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/bindings.kt +++ b/python-multiplatform/src/desktopMain/kotlin/python/native/ffi/bindings.kt @@ -927,7 +927,7 @@ inline fun PyDict_GetItemString(p: Long, key: String): Long { fun PyObject_GC_UnTrack(op: Long) = PyObject_GC_UnTrackHandle.invokeExact(op) as Unit init { - val P = Panama.POINTER_TYPE // Long.TYPE — represents a native pointer + val P = Panama.POINTER_TYPE // Long.TYPE, represents a native pointer // Load the native library first manager.loadLibPython() diff --git a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/NoFollowDeleteTest.kt b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/NoFollowDeleteTest.kt new file mode 100644 index 000000000..cb2a3723f --- /dev/null +++ b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/NoFollowDeleteTest.kt @@ -0,0 +1,39 @@ +package python.multiplatform.env + +import java.io.File +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** Issue #227: a delete must remove a symbolic link as a link, never what it points at. */ +class NoFollowDeleteTest { + + private fun scratch(): File = + Files.createTempDirectory(File("build").apply { mkdirs() }.absoluteFile.toPath(), "nofollow").toFile() + + @Test + fun aLinkToAnOutsideDirectoryIsRemovedButItsTargetSurvives() { + val work = scratch() + try { + val outside = File(work, "outside").apply { mkdirs() } + val keep = File(outside, "keep.txt").apply { writeText("keep") } + val victim = File(work, "victim").apply { mkdirs() } + File(victim, "own.txt").writeText("own") + Files.createSymbolicLink(File(victim, "escape").toPath(), outside.toPath()) + Files.createSymbolicLink(File(victim, "dangling").toPath(), File(work, "nowhere").toPath()) + + deleteWithoutFollowingLinks(victim) + + assertFalse(victim.exists(), "the directory itself should be gone") + assertTrue(keep.isFile, "the link was followed: its target's content was deleted") + } finally { + deleteWithoutFollowingLinks(work) + } + } + + @Test + fun aMissingPathIsNotAnError() { + deleteWithoutFollowingLinks(File(scratch(), "absent")) + } +} diff --git a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PackagedPythonHomeTest.kt b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PackagedPythonHomeTest.kt index f8ca69a82..ab3e351a2 100644 --- a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PackagedPythonHomeTest.kt +++ b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PackagedPythonHomeTest.kt @@ -40,6 +40,60 @@ class PackagedPythonHomeTest { // ---- resolution order ---- + // ---- beside a native-image executable (issue #246) ---- + + private val inNativeImage = PackagedPythonHome.NATIVE_IMAGE_CODE_PROPERTY to "runtime" + + @Test + fun aNativeImageFindsThePrefixBesideItsExecutable() { + // A native image has no java.home, no Compose resources directory and no launcher to set + // PYTHONHOME; the prefix ships beside the executable, as an iOS app's ships in its bundle. + val resolved = PackagedPythonHome.resolve( + environmentHome = null, + property = properties(inNativeImage), + isDirectory = { it == "/apps/demo/python-multiplatform-home" }, + executablePath = { "/apps/demo/demo" }, + ) + assertEquals("/apps/demo/python-multiplatform-home", resolved?.path) + } + + @Test + fun aJvmIgnoresADirectoryBesideItsExecutableAndNeverAsksForIt() { + // On a JVM the executable is `java`: a directory beside it belongs to the JDK, not to the app. + // And it is not even asked for: `ProcessHandle` does not exist on older Android. + var asked = false + val resolved = PackagedPythonHome.resolve( + environmentHome = null, + property = properties(), + isDirectory = { true }, + executablePath = { asked = true; "/jdk/bin/java" }, + ) + assertNull(resolved) + assertEquals(false, asked, "the executable must not be looked up outside a native image") + } + + @Test + fun aNativeImageWithNoPrefixBesideItResolvesToNothing() { + val resolved = PackagedPythonHome.resolve( + environmentHome = null, + property = properties(inNativeImage), + isDirectory = { false }, + executablePath = { "/apps/demo/demo" }, + ) + assertNull(resolved) + } + + @Test + fun anEnvironmentPythonHomeStillWinsInANativeImage() { + val resolved = PackagedPythonHome.resolve( + environmentHome = "/from/env", + property = properties(inNativeImage), + isDirectory = { true }, + executablePath = { "/apps/demo/demo" }, + ) + assertNull(resolved) + } + @Test fun anEnvironmentPythonHomeWinsAndNothingElseIsDone() { // CPython reads PYTHONHOME itself, and L-3 checks it; a second source would only be a diff --git a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PythonDevAgentTest.kt b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PythonDevAgentTest.kt new file mode 100644 index 000000000..0e73ec42c --- /dev/null +++ b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PythonDevAgentTest.kt @@ -0,0 +1,183 @@ +package python.multiplatform.env + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.PythonTestFixture +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.net.ConnectException +import java.net.ServerSocket +import java.net.Socket +import java.nio.file.Files +import java.util.concurrent.TimeUnit +import kotlin.system.exitProcess +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * SPEC L-12 (#233): the runtime loads the embedded dev agent ([DEV_AGENT_SOURCE], generated from + * `src/commonMain/python/python_multiplatform_devagent.py`) as a module of this interpreter, and it answers + * over a real loopback socket. The protocol itself is `src/test/python/test_devagent.py`'s. + */ +class PythonDevAgentTest { + /** As long as the agent's minimum (`secrets.token_urlsafe(32)`). */ + private val token = "a".repeat(22) + "b".repeat(21) + + @Test + fun `the embedded agent runs in this interpreter and answers over loopback`() = + PythonTestFixture.withInterpreter { + PythonDevAgent.load() + val overlay = Files.createTempDirectory("pmp-devagent").toFile() + Python3.exec( + "__import__('sys').modules['__main__'].__dict__['_pmp_devagent_test'] = " + + "__import__('${PythonDevAgent.MODULE}').start(0, '$token', '${overlay.absolutePath}')", + ) + val port = PythonTestFixture.eval("_pmp_devagent_test.port").toString().toInt() + try { + Socket("127.0.0.1", port).use { socket -> + socket.soTimeout = 10_000 + val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8)) + val out = socket.getOutputStream() + fun send(json: String) = out.write("$json\n".toByteArray(Charsets.UTF_8)).also { out.flush() } + + send("""{"id": 1, "op": "hello", "token": "$token"}""") + val hello = reader.readLine() + assertTrue("\"type\": \"hello\"" in hello && "\"protocol\": 1" in hello, hello) + + send("""{"id": 2, "op": "execute", "code": "import sys\nprint(sys.version_info[0])\n21 * 2"}""") + val replies = generateSequence { reader.readLine() }.takeWhile { it != null } + .takeWhileInclusive { "\"type\": \"done\"" !in it }.toList() + // print() writes the text and its newline as two writes, so as two stream messages. + assertTrue(replies.any { "\"name\": \"stdout\", \"text\": \"3\"" in it }, "$replies") + assertTrue(replies.any { "\"text/plain\": \"42\"" in it }, "$replies") + assertTrue("\"status\": \"ok\"" in replies.last(), "$replies") + } + } finally { + Python3.exec("_pmp_devagent_test.stop()") + } + } + + /** + * The hook in `Python3.initialize()`: a debug build (its payload carries [PythonDevAgent.ENABLE_MARKER]) + * started with [PythonDevAgent.ENVIRONMENT_VARIABLE] has an agent on that port with that token; one started + * without it has nothing listening. Each runs in its own JVM ([DevAgentProbe]), since an environment + * variable cannot be set in this one. + */ + @Test + fun `a process the launcher asked for an agent answers on the given port, and one it did not, does not`() { + val requested = launchProbe(request = true, debugBuild = true) + try { + requested.awaitLine("python-multiplatform dev agent: listening on 127.0.0.1:${requested.port}") + Socket("127.0.0.1", requested.port).use { socket -> + socket.soTimeout = 10_000 + val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8)) + val out = socket.getOutputStream() + out.write("{\"id\": 1, \"op\": \"hello\", \"token\": \"$token\"}\n".toByteArray()) + out.write("{\"id\": 2, \"op\": \"execute\", \"code\": \"6 * 7\"}\n".toByteArray()) + out.flush() + assertTrue("\"type\": \"hello\"" in reader.readLine()) + assertTrue("\"text/plain\": \"42\"" in reader.readLine()) + } + } finally { + requested.process.destroyForcibly() + } + + val notRequested = launchProbe(request = false, debugBuild = true) + try { + notRequested.awaitLine(DevAgentProbe.READY) + assertFailsWith { Socket("127.0.0.1", notRequested.port).close() } + } finally { + notRequested.process.destroyForcibly() + } + } + + /** + * A release build (its payload has no [PythonDevAgent.ENABLE_MARKER]) opens no port even when its process + * is started with the variable: "a release launch never sets it" is the launcher's promise, not this + * runtime's guarantee, so the runtime does not rely on it. + */ + @Test + fun `a release build opens no port even when the variable is set`() { + val release = launchProbe(request = true, debugBuild = false) + try { + release.awaitLine(DevAgentProbe.READY) + assertFailsWith { Socket("127.0.0.1", release.port).close() } + assertTrue(release.log.readLines().none { "listening" in it }, release.log.readText()) + assertTrue(release.log.readText().contains(PythonDevAgent.ENABLE_MARKER), release.log.readText()) + } finally { + release.process.destroyForcibly() + } + } + + /** A token shorter than the agent's minimum is refused before anything listens. */ + @Test + fun `a debug build refuses a short token and opens no port`() { + val short = launchProbe(request = true, debugBuild = true, token = "short-token") + try { + short.awaitLine(DevAgentProbe.READY) + assertFailsWith { Socket("127.0.0.1", short.port).close() } + assertTrue(short.log.readText().contains("refusing to start"), short.log.readText()) + } finally { + short.process.destroyForcibly() + } + } + + private class Probe(val process: Process, val port: Int, val log: File) { + fun awaitLine(line: String) { + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(120) + while (System.nanoTime() < deadline) { + if (log.exists() && log.readLines().any { it.trim() == line }) return + if (!process.isAlive) break + Thread.sleep(100) + } + throw AssertionError("never saw '$line':\n${if (log.exists()) log.readText() else "(no output)"}") + } + } + + /** + * [debugBuild] stages a payload as toolchain does for a debug variant: a `python/` root on the class path + * with a module and the marker. A release variant's payload is the same without the marker. + */ + private fun launchProbe(request: Boolean, debugBuild: Boolean, token: String = this.token): Probe { + val port = ServerSocket(0).use { it.localPort } + val workDir = Files.createTempDirectory("pmp-devagent-probe").toFile() + val log = File(workDir, "probe.log") + val payload = File(workDir, "classes/${PythonPayload.PAYLOAD_ROOT}").apply { mkdirs() } + File(payload, "devagent_probe_app.py").writeText("VALUE = 1\n") + if (debugBuild) File(payload, PythonDevAgent.ENABLE_MARKER).writeText("") + val command = listOf( + File(System.getProperty("java.home"), "bin/java").path, + "--enable-preview", + "--enable-native-access=ALL-UNNAMED", + "-cp", + File(workDir, "classes").path + File.pathSeparator + System.getProperty("java.class.path"), + DevAgentProbe::class.java.name, + ) + val builder = ProcessBuilder(command).directory(workDir).redirectErrorStream(true).redirectOutput(log) + builder.environment().remove(PythonDevAgent.ENVIRONMENT_VARIABLE) + if (request) builder.environment()[PythonDevAgent.ENVIRONMENT_VARIABLE] = "port=$port;token=$token" + return Probe(builder.start(), port, log) + } + + private fun Sequence.takeWhileInclusive(predicate: (T) -> Boolean): Sequence = sequence { + for (item in this@takeWhileInclusive) { + yield(item) + if (!predicate(item)) break + } + } +} + +/** A JVM that starts Python and stays up, for [PythonDevAgentTest]'s launch test. */ +object DevAgentProbe { + const val READY = "DEV_AGENT_PROBE_READY" + + @JvmStatic + fun main(args: Array) { + Python3.initialize(silent = true) + println(READY) + System.out.flush() + Thread.sleep(120_000) + exitProcess(0) + } +} diff --git a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PythonPayloadTest.kt b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PythonPayloadTest.kt index 2fa2c2fc9..93f6d3d79 100644 --- a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PythonPayloadTest.kt +++ b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/env/PythonPayloadTest.kt @@ -84,6 +84,27 @@ class PythonPayloadTest { ) } + // ------------------------------------------------------------ re-extraction keeps links' targets + + @Test + fun reExtractingOverAnIncompleteTreeDoesNotDeleteWhatALinkInsideItPointsAt() { + val jar = payloadJar("payload_jar_nofollow", withDirectoryEntries = true) + val cache = cacheDirectory() + val loader = URLClassLoader(arrayOf(jar.toURI().toURL()), null) + val root = ClasspathPayload.resolveRoots(loader, cache).single() + + // Make the tree look unfinished (no stamp) and plant a link to a directory outside it. + val outside = newTemporaryDirectory("outside") + val keep = File(outside, "keep.txt").apply { writeText("keep") } + assertTrue(ClasspathPayload.stampFileFor(root).delete(), "expected a stamp to have been written") + java.nio.file.Files.createSymbolicLink(File(root, "escape").toPath(), outside.toPath()) + + ClasspathPayload.resolveRoots(loader, cache) + + assertTrue(keep.isFile, "the stale tree was replaced, but the link in it was followed and its target deleted") + assertFalse(File(root, "escape").exists(), "the stale tree should have been replaced") + } + // ---------------------------------------------------------------------- discovery: jar entries @Test @@ -249,7 +270,7 @@ class PythonPayloadTest { * **This library's own top-level Kotlin package is `python`.** * * `python.multiplatform` compiles to class files under `python/multiplatform`, so every class path this - * library is on answers `getResources("python/")` — with `build/classes/kotlin/desktop/main/python` + * library is on answers `getResources("python/")`, with `build/classes/kotlin/desktop/main/python` * during a Gradle build, and with a `jar:` URL into `python-multiplatform.jar` once published. * Name-based discovery therefore puts a directory of `.class` files at `sys.path[0]` in every * application that uses this library. It is not hypothetical: this test failed on the first run diff --git a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/KotlinNamedSurfaceTest.kt b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/KotlinNamedSurfaceTest.kt index 3cf8906a4..bfaac20c2 100644 --- a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/KotlinNamedSurfaceTest.kt +++ b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/KotlinNamedSurfaceTest.kt @@ -18,11 +18,12 @@ import kotlin.test.assertTrue * * `docs/INTENT.md` §2.2 and §2.3, as executable statements: * - * - A Kotlin declaration is reached under its Kotlin name, takes keyword arguments by its **Kotlin - * parameter names** and leaves an omitted defaulted parameter to Kotlin's own default. Nothing is - * snake_cased: `fillMaxWidth` is `fillMaxWidth`, and `fill_max_width` does not exist. + * - A Kotlin package is a module under its Kotlin name and nothing else: no namespace is renamed. + * - Inside it, a Kotlin declaration is reached under its Kotlin name **and** its Pythonic alias + * (`fillMaxWidth` / `fill_max_width`, issue #131), takes keyword arguments by either spelling of a + * parameter, and leaves an omitted defaulted parameter to Kotlin's own default. * - `pythonx` is a real package in pythonx-compose. The binder neither creates a `pythonx` module - * nor prevents one on disk from loading. + * nor prevents one on disk from loading -- and such a package can build on the aliases. * - Every function the binder puts on a Kotlin-named module carries its signature as public metadata * (`inspect.signature`, and `python_multiplatform.describe`) so a Pythonic layer can build on it * by rule rather than by a wrapper per declaration. @@ -123,46 +124,97 @@ class KotlinNamedSurfaceTest { assertTrue("horizontal_padding" in eval("_kns['unknown']"), eval("_kns['unknown']")) } - /** No snake_case anywhere: not as a member, not as a keyword. */ + /** + * Issue #131: a Kotlin name and its Pythonic alias are one declaration, and a keyword is accepted + * by either spelling -- whichever installer ran. (`238119b7` had made the snake_case member an + * `AttributeError` and the snake_case keyword a `TypeError`; this is the reversal of exactly + * those two assertions.) + */ @Test - fun aKotlinNameIsNotSnakeCased() = withBothInstalled { + fun aKotlinNameAndItsPythonicAliasAreOneDeclaration() = withBothInstalled { Python3.exec( """ + import python_multiplatform as _kns_pm import androidx.compose.foundation.layout as _kns_layout - from androidx.compose.ui import emptyModifier, describeModifier - _kns = {'kotlin': describeModifier(_kns_layout.fillMaxWidth(emptyModifier()))} - try: - _kns_layout.fill_max_width - _kns['snake_member'] = 'resolved' - except AttributeError: - _kns['snake_member'] = 'AttributeError' - try: - _kns_layout.padding__Dp_Dp(emptyModifier(), vertical=1.0) - _kns['kotlin_keyword'] = 'accepted' - except TypeError as _kns_e: - _kns['kotlin_keyword'] = str(_kns_e) - try: - _kns_layout.padding__PaddingValues(emptyModifier(), padding_values=None) - _kns['snake_keyword'] = 'accepted' - except TypeError as _kns_e: - _kns['snake_keyword'] = 'TypeError' - import python_multiplatform.binding as _kns_binding - _kns['renamers'] = [ - _n for _n in ('to_python_name', 'to_kotlin_name') if hasattr(_kns_binding, _n) - ] + from androidx.compose.ui import emptyModifier, describeModifier, empty_modifier, describe_modifier + _kns = { + 'kotlin': describeModifier(_kns_layout.fillMaxWidth(emptyModifier())), + 'snake': describe_modifier(_kns_layout.fill_max_width(empty_modifier())), + 'same': _kns_layout.fill_max_width is _kns_layout.fillMaxWidth, + 'kotlin_keyword': describeModifier(_kns_layout.padding__Dp_Dp(emptyModifier(), vertical=1.0)), + 'snake_keyword': describeModifier(_kns_layout.padding__PaddingValues( + emptyModifier(), padding_values=_kns_layout.padding_values_of(2))), + 'kotlin_keyword_pv': describeModifier(_kns_layout.padding__PaddingValues( + emptyModifier(), paddingValues=_kns_layout.paddingValuesOf(3))), + 'rule': _kns_pm.python_name('rememberTextFieldState'), + 'dir': [_n for _n in dir(_kns_layout) if _n in ('fill_max_width', 'padding_values_of')], + } """.trimIndent(), ) assertEquals("fillMaxWidth", eval("_kns['kotlin']")) - assertEquals("AttributeError", eval("_kns['snake_member']")) - assertEquals("accepted", eval("_kns['kotlin_keyword']")) - assertEquals("TypeError", eval("_kns['snake_keyword']")) - assertEquals("[]", eval("repr(_kns['renamers'])")) + assertEquals("fillMaxWidth", eval("_kns['snake']")) + assertEquals("True", eval("_kns['same']")) + assertEquals("padding(v=1.0)", eval("_kns['kotlin_keyword']")) + assertEquals("padding(pv(2.0))", eval("_kns['snake_keyword']")) + assertEquals("padding(pv(3.0))", eval("_kns['kotlin_keyword_pv']")) + assertEquals("remember_text_field_state", eval("_kns['rule']")) + assertEquals("[]", eval("repr(_kns['dir'])"), "dir() lists each declaration once, by its Kotlin name") + } + + /** + * AGENTS.md §12.2 with the aliases in place: a real `pythonx` package on disk loads, and its own + * code reaches the Kotlin-named module by the Pythonic names -- the namespace it imports is + * `androidx.*`, untouched, and the binder has made no `pythonx` module of its own. + */ + @Test + fun aRealPythonxPackageOnDiskBuildsOnThePythonicNames() = PythonTestFixture.withInterpreter { + installAdapter() + PythonProxySource.install() + Python3.exec( + """ + import os as _kns_os, sys as _kns_sys + _kns = { + 'occupied': sorted( + _n for _n in _kns_sys.modules if _n == 'pythonx' or _n.startswith('pythonx.') + ), + } + _kns_root = _kns_os.path.abspath( + _kns_os.path.join('build', 'tmp', 'kotlin-named-surface-pythonic', str(_kns_os.getpid())) + ) + _kns_os.makedirs(_kns_os.path.join(_kns_root, 'pythonx'), exist_ok=True) + with open(_kns_os.path.join(_kns_root, 'pythonx', '__init__.py'), 'w') as _kns_f: + _kns_f.write( + "from androidx.compose.foundation.layout import fill_max_width\n" + "from androidx.compose.ui import empty_modifier, describe_modifier\n" + "MARK = 'on-disk'\n" + "def full_width():\n" + " return describe_modifier(fill_max_width(empty_modifier()))\n" + ) + _kns_sys.path.insert(0, _kns_root) + try: + import pythonx as _kns_px + _kns['mark'] = getattr(_kns_px, 'MARK', None) + _kns['from_disk'] = (getattr(_kns_px, '__file__', None) or '').startswith(_kns_root) + _kns['works'] = _kns_px.full_width() + finally: + _kns_sys.path.remove(_kns_root) + if getattr(_kns_sys.modules.get('pythonx'), 'MARK', None) == 'on-disk': + del _kns_sys.modules['pythonx'] + import shutil as _kns_shutil + _kns_shutil.rmtree(_kns_root, ignore_errors=True) + """.trimIndent(), + ) + assertEquals("[]", eval("repr(_kns['occupied'])"), "the binder must not create any pythonx module") + assertEquals("on-disk", eval("_kns['mark']")) + assertEquals("True", eval("_kns['from_disk']")) + assertEquals("fillMaxWidth", eval("_kns['works']")) } /** - * The public metadata contract a Pythonic layer builds on: `inspect.signature` shows the Kotlin - * parameter names with defaults marked, and `python_multiplatform.describe` gives the overload - * set with per-parameter Kotlin types. + * The public metadata contract a Pythonic layer builds on: `inspect.signature` shows each + * parameter's Pythonic keyword (the Kotlin name where it has no alias) with defaults marked, and + * `python_multiplatform.describe` gives the overload set with per-parameter Kotlin names, Pythonic + * names and Kotlin types. * * Asserted for the proxy-rendered function and for the binding layer's own, in both install * orders, so the answer cannot depend on who got there first. @@ -190,6 +242,11 @@ class KotlinNamedSurfaceTest { for _p in _kns_sig.parameters.values() ], 'annotation': _kns_sig.parameters['vertical'].annotation, + 'pythonic': list(_kns_inspect.signature(_kns_layout.padding__PaddingValues).parameters), + 'both_names': [ + (_q['name'], _q['python_name']) + for _q in _kns_pm.describe(_kns_layout.padding__PaddingValues)[0]['parameters'] + ], 'one': [ (_d['name'], [(_q['name'], _q['type'], _q['has_default']) for _q in _d['parameters']], _d['receiver']) for _d in _kns_pm.describe(_kns_layout.padding__Dp_Dp) @@ -206,6 +263,9 @@ class KotlinNamedSurfaceTest { order, ) assertEquals("androidx.compose.ui.unit.Dp", eval("_kns['annotation']"), order) + // Issue #131: the signature shows the Pythonic keyword, `describe` both spellings. + assertEquals("['receiver', 'padding_values']", eval("repr(_kns['pythonic'])"), order) + assertEquals("[('paddingValues', 'padding_values')]", eval("repr(_kns['both_names'])"), order) assertEquals( "[('androidx.compose.foundation.layout.padding__Dp_Dp', " + "[('horizontal', 'androidx.compose.ui.unit.Dp', True), ('vertical', 'androidx.compose.ui.unit.Dp', True)], " + diff --git a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/MemberResolverTest.kt b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/MemberResolverTest.kt index 0bac2e693..dab2f3677 100644 --- a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/MemberResolverTest.kt +++ b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/MemberResolverTest.kt @@ -14,11 +14,13 @@ import kotlin.test.assertTrue /** * `python_multiplatform.binding.add_member_resolver` (issue #17): the one public point where a * Pythonic package built on the binder (pythonx-compose) can say what a Kotlin proxy's member is - * called, without the binder renaming anything itself. + * called. Since issue #131 the binder also serves its own Pythonic alias (`fill_max_width`), asked + * **after** the resolvers, so a resolver still decides what a name means wherever it answers. * * Contract under test: * - * - With no resolver registered, an unknown name on a proxy is an `AttributeError`, exactly as before. + * - With no resolver registered, a snake_case alias is the binder's own (issue #131), and a name that + * is no alias either is an `AttributeError`. * - `fn(kotlin_type_name, requested_name, kotlin_member_names) -> kotlin_name | None` is asked only * when no Kotlin member of the requested name exists; the Kotlin member it names is served. * - The binder's proxy classes are not written to: `dir()` and the class `__dict__` show Kotlin names only. @@ -41,22 +43,28 @@ class MemberResolverTest { HandleTable.releaseAll() } + /** Issue #131: with no resolver, the binder's own alias serves the snake_case name; nonsense stays an error. */ @Test - fun withNoResolverASnakeNameIsAnAttributeError() = withAdapter { + fun withNoResolverASnakeNameIsTheBindersOwnAlias() = withAdapter { Python3.exec( """ - from androidx.compose.ui import Modifier + from androidx.compose.ui import Modifier, describeModifier _mr = {} for _label, _target in (('instance', Modifier.padding(16)), ('class', Modifier)): try: - _target.fill_max_width - _mr[_label] = 'resolved' + _mr[_label] = describeModifier(_target.fill_max_width()) except AttributeError: _mr[_label] = 'AttributeError' + try: + Modifier.padding(16).fill_max_nothing + _mr['nonsense'] = 'resolved' + except AttributeError: + _mr['nonsense'] = 'AttributeError' """.trimIndent(), ) - assertEquals("AttributeError", eval("_mr['instance']")) - assertEquals("AttributeError", eval("_mr['class']")) + assertEquals("padding(16.0) -> fillMaxWidth", eval("_mr['instance']")) + assertEquals("fillMaxWidth", eval("_mr['class']")) + assertEquals("AttributeError", eval("_mr['nonsense']")) } @Test @@ -175,17 +183,17 @@ class MemberResolverTest { assertEquals("True", eval("_mr['class_dict_same']")) assertEquals("True", eval("_mr['dir_same']")) } - // Removing the only resolver takes the alias with it: nothing was left on the class. + // Removing the only resolver takes its alias with it: nothing was left on the class. The name + // still resolves -- now as the binder's own alias (issue #131) -- and that one is not written + // onto the class either. Python3.exec( """ - try: - Modifier.padding(1).fill_max_width - _mr = {'after': 'still resolved'} - except AttributeError: - _mr = {'after': 'AttributeError'} + _mr = {'after': 'still resolved' if callable(Modifier.padding(1).fill_max_width) else 'not callable'} + _mr['alias_in_class'] = 'fill_max_width' in vars(type(Modifier.padding(1))) """.trimIndent(), ) - assertEquals("AttributeError", eval("_mr['after']")) + assertEquals("still resolved", eval("_mr['after']")) + assertEquals("False", eval("_mr['alias_in_class']")) } // ------------------------------------------------------------------------------- keyword maps (#34) @@ -257,21 +265,23 @@ class MemberResolverTest { } } - /** No resolver: a Python-spelled keyword on a member is refused exactly as before. */ + /** No resolver: a snake_case keyword on a member is the binder's own keyword alias (issue #131). */ @Test - fun withNoResolverAKeywordIsNotMapped() = withAdapter { + fun withNoResolverASnakeKeywordIsTheBindersOwnAlias() = withAdapter { Python3.exec( """ - from androidx.compose.ui import Modifier + from androidx.compose.ui import Modifier, describeModifier from androidx.compose.foundation.layout import paddingValuesOf + _mr = {'outcome': describeModifier(Modifier.padding(1).padding(padding_values=paddingValuesOf(4)))} try: - Modifier.padding(1).padding(padding_values=paddingValuesOf(4)) - _mr = {'outcome': 'accepted'} + Modifier.padding(1).padding(padding_valuez=paddingValuesOf(4)) + _mr['typo'] = 'accepted' except TypeError: - _mr = {'outcome': 'TypeError'} + _mr['typo'] = 'TypeError' """.trimIndent(), ) - assertEquals("TypeError", eval("_mr['outcome']")) + assertEquals("padding(1.0) -> padding(pv(4.0))", eval("_mr['outcome']")) + assertEquals("TypeError", eval("_mr['typo']")) } // ------------------------------------------------------------------------------- helpers diff --git a/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/PythonxComposeCompatibilityTest.kt b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/PythonxComposeCompatibilityTest.kt new file mode 100644 index 000000000..1b303078f --- /dev/null +++ b/python-multiplatform/src/desktopTest/kotlin/python/multiplatform/ffi/pythonx/PythonxComposeCompatibilityTest.kt @@ -0,0 +1,219 @@ +package python.multiplatform.ffi.pythonx + +import python.multiplatform.ffi.Python3 +import python.multiplatform.ffi.PythonTestFixture +import python.multiplatform.ffi.upcall.PythonProxySource +import python.multiplatform.reflection.HandleTable +import python.multiplatform.reflection.UpcallTable +import python.native.ffi.bindUpcallOrNull +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * pythonx-compose 0.1.0a1 against the binder after issue #131. + * + * pythonx-compose ships its own name-conversion layer (`pythonx/compose/_reexport.py`) and will drop + * it once the binder converts names itself. Until it does, the two run together, and this pins that + * they do not fight: + * + * - its member resolver (`member_name`) is asked **before** the binder's own alias, so on a proxy it + * still answers every snake_case name it answered before, with the same Kotlin member; + * - its keyword maps (`{snake: kotlinParam}`) reach the binder as Kotlin names, which the binder takes + * as it always did -- no double conversion, and writing both spellings is still "two values"; + * - its `_name_table(kotlin_module)` converts `dir()` by the same rule and **raises** when two entries + * map to one name, so the binder's `dir()` must list each declaration once, by its Kotlin name. A + * `dir()` that also listed `fill_max_width` would make every pythonx module fail; + * - its `PythonicFunction` snake_cases `inspect.signature`'s names, which is a no-op on names the + * binder already shows snake_cased. + * + * The functions below are copied from pythonx-compose 0.1.0a1 (`_reexport.py`: `snake_case`, + * `python_name`, `_method_keywords`, `member_name`, `_name_table`, `PythonicFunction`), trimmed of + * docstrings and of the manifest, which this test does not need. They are a pin of that release's + * behaviour, not a dependency: the file lives in another repository. + */ +class PythonxComposeCompatibilityTest { + + @BeforeTest + fun install() { + check(PythonTestFixture.available) { + "CPython could not be initialized (${PythonTestFixture.failureReason})" + } + UpcallTable.install(listOf(ComposeShapedFragment)) + ComposeShapedFragment.calls.clear() + assertTrue(bindUpcallOrNull(ComposeShapedFragment.EMPTY_MODIFIER), "the fixture table is not installed") + } + + @AfterTest + fun cleanup() { + UpcallTable.clear() + HandleTable.releaseAll() + } + + @Test + fun theReleasedResolverStillDecidesAProxyNameAndItsKeywords() = withPythonx { + Python3.exec( + """ + from androidx.compose.ui import Modifier, describeModifier + from androidx.compose.foundation.layout import paddingValuesOf + _pxc_calls.clear() + _pxc = { + 'chain': describeModifier(Modifier.padding(16).fill_max_width().z_index(2)), + 'asked': sorted(set(_c[1] for _c in _pxc_calls)), + 'keyword': describeModifier(Modifier.padding(1).padding(padding_values=paddingValuesOf(4))), + 'kotlin_keyword': describeModifier(Modifier.padding(1).padding(paddingValues=paddingValuesOf(5))), + } + try: + Modifier.padding(1).padding(padding_values=paddingValuesOf(4), paddingValues=paddingValuesOf(4)) + _pxc['both'] = 'accepted' + except TypeError: + _pxc['both'] = 'TypeError' + """.trimIndent(), + ) + assertEquals("padding(16.0) -> fillMaxWidth -> zIndex(2.0)", eval("_pxc['chain']")) + val asked = eval("repr(_pxc['asked'])") + assertTrue("'fill_max_width'" in asked && "'z_index'" in asked, "the released resolver was not asked first: $asked") + assertEquals("padding(1.0) -> padding(pv(4.0))", eval("_pxc['keyword']")) + assertEquals("padding(1.0) -> padding(pv(5.0))", eval("_pxc['kotlin_keyword']")) + assertEquals("TypeError", eval("_pxc['both']")) + } + + @Test + fun theReleasedNameTableStillReadsTheBindersDirWithoutACollision() = withPythonx { + Python3.exec( + """ + import inspect as _pxc_inspect + import androidx.compose.foundation.layout as _pxc_layout + import androidx.compose.ui as _pxc_ui + _pxc = {} + try: + _pxc_table = _pxc_name_table(_pxc_layout) + _pxc['table'] = sorted((_k, _v) for _k, _v in _pxc_table.items() if _k != _v) + _pxc_name_table(_pxc_ui) + _pxc['raised'] = None + except AttributeError as _pxc_e: + _pxc['raised'] = str(_pxc_e) + _pxc_fn = _PxcPythonicFunction(_pxc_layout.padding__PaddingValues, 'padding__PaddingValues') + _pxc['signature'] = list(_pxc_inspect.signature(_pxc_fn).parameters) + _pxc['call'] = _pxc_ui.describeModifier( + _pxc_fn(_pxc_ui.emptyModifier(), padding_values=_pxc_layout.paddingValuesOf(6))) + """.trimIndent(), + ) + assertEquals("None", eval("repr(_pxc['raised'])"), "pythonx's _name_table must not see two names for one declaration") + assertTrue("('fill_max_width', 'fillMaxWidth')" in eval("repr(_pxc['table'])"), eval("repr(_pxc['table'])")) + assertEquals("['receiver', 'padding_values']", eval("repr(_pxc['signature'])")) + assertEquals("padding(pv(6.0))", eval("_pxc['call']")) + } + + /** Installs both binder layers and registers pythonx-compose 0.1.0a1's resolver, recording its questions. */ + private inline fun withPythonx(block: () -> Unit) = PythonTestFixture.withInterpreter { + PythonxAdapter.install(COMPOSE_SHAPED_RAW_VALUE_CLASSES) + Python3.exec( + "import python_multiplatform.binding as _pm_binding\n" + + "_pm_binding.register_empty('androidx.compose.ui.Modifier', '${ComposeShapedFragment.EMPTY_MODIFIER}')", + ) + PythonProxySource.install() + Python3.exec(RELEASED_REEXPORT) + try { + block() + } finally { + Python3.exec("_pm_binding.remove_member_resolver(_pxc_member_name)") + } + } + + private fun eval(expression: String): String = PythonTestFixture.eval(expression).toString() + + private companion object { + /** pythonx-compose 0.1.0a1's rule and resolver, as released; see the class KDoc. */ + val RELEASED_REEXPORT = """ + import inspect as _pxc_inspect_mod + import re as _pxc_re + import sys as _pxc_sys + import python_multiplatform.binding as _pm_binding + + _PXC_LOWER_UPPER = _pxc_re.compile(r"([a-z0-9])([A-Z])") + _PXC_ACRONYM_WORD = _pxc_re.compile(r"([A-Z]+)([A-Z][a-z])") + + def _pxc_snake_case(kotlin_name): + return _PXC_LOWER_UPPER.sub(r"\1_\2", _PXC_ACRONYM_WORD.sub(r"\1_\2", kotlin_name)).lower() + + def _pxc_python_name(kotlin_name): + if kotlin_name[:1].isupper(): + return kotlin_name + base, sep, suffix = kotlin_name.partition("__") + return _pxc_snake_case(base) + sep + suffix + + def _pxc_method_keywords(kotlin_type_name, kotlin_member_name): + describe_member = getattr(_pxc_sys.modules.get("python_multiplatform"), "describe_member", None) + if describe_member is None: + return {} + keywords = {} + try: + declarations = describe_member(kotlin_type_name, kotlin_member_name) + except (TypeError, AttributeError): + declarations = () + for declaration in declarations: + for parameter in declaration.get("parameters", ()): + kotlin_parameter = parameter.get("name") + if kotlin_parameter and _pxc_snake_case(kotlin_parameter) != kotlin_parameter: + keywords.setdefault(_pxc_snake_case(kotlin_parameter), kotlin_parameter) + return keywords + + _pxc_calls = [] + + def _pxc_member_name(kotlin_type_name, requested, kotlin_member_names): + _pxc_calls.append((kotlin_type_name, requested)) + if requested in kotlin_member_names: + target = requested + else: + matches = [name for name in kotlin_member_names if _pxc_python_name(name) == requested] + target = matches[0] if len(matches) == 1 else None + if target is None: + return None + keywords = _pxc_method_keywords(kotlin_type_name, target) + return (target, keywords) if keywords else target + + def _pxc_name_table(kotlin): + table = {} + for kotlin_name in dir(kotlin): + if kotlin_name.startswith("_"): + continue + pythonic = _pxc_python_name(kotlin_name) + other = table.get(pythonic) + if other is not None and other != kotlin_name: + raise AttributeError( + repr(pythonic) + " would name both " + repr(other) + " and " + repr(kotlin_name) + ) + table[pythonic] = kotlin_name + return table + + class _PxcPythonicFunction: + def __init__(self, kotlin_fn, name): + self._kotlin = kotlin_fn + self.__name__ = name + keywords = {} + for row in _pxc_sys.modules["python_multiplatform"].describe(kotlin_fn): + for parameter in row.get("parameters", ()): + keywords.setdefault(_pxc_snake_case(parameter["name"]), parameter["name"]) + self._keywords = keywords + + @property + def __signature__(self): + kotlin_signature = _pxc_inspect_mod.signature(self._kotlin) + return kotlin_signature.replace(parameters=[ + parameter if parameter.kind in (parameter.VAR_POSITIONAL, parameter.VAR_KEYWORD) + else parameter.replace(name=_pxc_snake_case(parameter.name)) + for parameter in kotlin_signature.parameters.values() + ]) + + def __call__(self, *args, **kwargs): + if kwargs: + kwargs = {self._keywords.get(key, key): value for key, value in kwargs.items()} + return self._kotlin(*args, **kwargs) + + _pm_binding.add_member_resolver(_pxc_member_name) + """.trimIndent() + } +} diff --git a/python-multiplatform/src/iosMain/README.md b/python-multiplatform/src/iosMain/README.md index 883b8e3a3..fbc3c9853 100644 --- a/python-multiplatform/src/iosMain/README.md +++ b/python-multiplatform/src/iosMain/README.md @@ -1,4 +1,4 @@ -# iosMain — rules +# iosMain, rules iOS-specific sources. Depends on `nativeMain`; CPython arrives as `Python.xcframework`. @@ -6,7 +6,7 @@ iOS-specific sources. Depends on `nativeMain`; CPython arrives as `Python.xcfram `Python.xcframework` contains the interpreter binary and headers and nothing else. `Py_Initialize()` needs `PYTHONHOME` to point at the stdlib on disk, and there are two different -ways it fails to if that's wrong — which one you get depends on *what's running*, not just on what +ways it fails to if that's wrong, which one you get depends on *what's running*, not just on what `PYTHONHOME` says, and only one of them is easy to diagnose. **A missing or empty prefix aborts.** `Py_Initialize()` calls `Py_FatalError()` and kills the @@ -16,10 +16,10 @@ exception, no interpreter left afterward, but at least it is fast and it says wh `python.multiplatform.env.PythonHomeCheck`) that catches this shape of misconfiguration and turns it into a catchable `IllegalStateException` naming the path instead. -**A `PYTHONHOME` under a sandboxed path can hang instead — with 0% CPU and nothing in the +**A `PYTHONHOME` under a sandboxed path can hang instead, with 0% CPU and nothing in the console.** Reproduced directly (ROADMAP, 2026-08-13, "PYTHONHOME on an external volume hangs the app instead of failing it"): the *installed simulator app* parked forever inside `open$NOCANCEL` -importing `encodings` from a path under `/Volumes/` — a blank window, no sandbox denial, no Python +importing `encodings` from a path under `/Volumes/`, a blank window, no sandbox denial, no Python error anywhere in the log. `PythonHomeCheck`'s own filesystem probe cannot promise to catch this either: it reads the same directory through the same kind of syscall CPython's import machinery does, and a path whose access is *parked* rather than *denied* can park the check identically. @@ -27,13 +27,13 @@ does, and a path whose access is *parked* rather than *denied* can park the chec **The test binary and an installed app get their stdlib differently.** The build extracts the stdlib into `build/python-stdlib/`, and the simulator *test* task points `SIMCTL_CHILD_PYTHONHOME` at it (`simctl` only forwards variables prefixed `SIMCTL_CHILD_`). That path is on this workspace's -external volume — fine for the test binary, which `simctl` launches with fewer sandbox restrictions +external volume, fine for the test binary, which `simctl` launches with fewer sandbox restrictions than an installed app, and exactly the shape that hangs an app rather than starting it. An installed **app** carries its own prefix inside its bundle (SPEC L-11, `docs/platforms/ios-app-bundle.md`): -- `stageIosPythonHome__` stages the stdlib for one slice; `tools/xcode/install-python.sh`, +- `stageIosPythonHome__` stages the stdlib for one slice; `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`, an Xcode Run Script phase, copies it to `/python-multiplatform-home/`, copies the consumer's payload to `/python/`, and wraps every `.so` as `Frameworks/.framework` with a `.fwork` placeholder (Apple loads no loose binaries; CPython's `AppleFrameworkLoader` follows the diff --git a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt index 6398f4e82..402345076 100644 --- a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt +++ b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PackagedPythonHome.ios.kt @@ -16,7 +16,7 @@ import platform.Foundation.NSFileManager * (or `simctl launch`) with no `PYTHONHOME`. The simulator *test* task sets one, pointing at the * workspace (`SIMCTL_CHILD_PYTHONHOME`), but the same shape for an app parks it at 0% CPU when the * path is on an external volume (`iosMain/README.md`). So the app carries its prefix inside its own - * bundle -- `tools/xcode/install-python.sh` copies it to `/`[DIRECTORY_NAME] -- and it is handed + * bundle -- `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh` copies it to `/`[DIRECTORY_NAME] -- and it is handed * to CPython in-process, before `Py_Initialize`, by [applyPackagedPythonHome]. * * The desktop counterpart is `PackagedPythonHome` (`jvmMain`, SPEC L-9); same order, same name. diff --git a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt index 2446ace6f..c01d22bd6 100644 --- a/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt +++ b/python-multiplatform/src/iosMain/kotlin/python/multiplatform/env/PythonPayload.ios.kt @@ -7,7 +7,7 @@ import platform.Foundation.NSBundle * directory. * * `toolchain` produces the payload (`build/pythonStaging/ios/python`, or the app's own sources); - * `tools/xcode/install-python.sh`, an Xcode Run Script phase, copies it there and wraps any + * `python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh`, an Xcode Run Script phase, copies it there and wraps any * extension module in it as a framework (SPEC L-11, `docs/platforms/ios-app-bundle.md`). An app * built without that phase has no `python/`, and this returns an empty list. * diff --git a/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/env/NoFollowDelete.jvm.kt b/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/env/NoFollowDelete.jvm.kt new file mode 100644 index 000000000..904a43743 --- /dev/null +++ b/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/env/NoFollowDelete.jvm.kt @@ -0,0 +1,32 @@ +package python.multiplatform.env + +import java.io.File +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +/** + * Deletes [path] and everything under it without following symbolic links (issue #227). Shared by the Android and desktop payload staging (jvmMain; Android minSdk is 26, so java.nio.file is available). + * + * Kotlin's `File.deleteRecursively` descends into a directory symlink and deletes what it points + * at. `Files.walkFileTree` without `FOLLOW_LINKS` reports a link as a file-like entry, so the link + * itself is removed and its target is left alone. A missing path is not an error. + */ +internal fun deleteWithoutFollowingLinks(path: File) { + val start = path.toPath() + if (!Files.exists(start, java.nio.file.LinkOption.NOFOLLOW_LINKS)) return + Files.walkFileTree(start, object : SimpleFileVisitor() { + override fun visitFile(file: java.nio.file.Path, attrs: BasicFileAttributes): FileVisitResult { + Files.delete(file) + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: java.nio.file.Path, exc: IOException?): FileVisitResult { + if (exc != null) throw exc + Files.delete(dir) + return FileVisitResult.CONTINUE + } + }) +} diff --git a/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/env/PackagedPythonHome.jvm.kt b/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/env/PackagedPythonHome.jvm.kt index 4cc21bb98..805d696f8 100644 --- a/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/env/PackagedPythonHome.jvm.kt +++ b/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/env/PackagedPythonHome.jvm.kt @@ -32,6 +32,12 @@ internal object PackagedPythonHome { /** `PACKAGED_HOME_DIRECTORY` in `python-multiplatform-gradle-plugin`; the two must agree. */ const val DIRECTORY_NAME = "python-multiplatform-home" + /** + * GraalVM sets this to `runtime` inside a native image (and to `buildtime` while building one); + * on a JVM it is absent. Read as a property, so nothing here depends on GraalVM's SDK. + */ + const val NATIVE_IMAGE_CODE_PROPERTY = "org.graalvm.nativeimage.imagecode" + /** A resolved prefix and a human-readable account of where it came from, for error messages. */ data class Resolved(val path: String, val source: String) @@ -40,6 +46,8 @@ internal object PackagedPythonHome { environmentHome = System.getenv("PYTHONHOME"), property = System::getProperty, isDirectory = { File(it).isDirectory }, + // A lambda: `ProcessHandle` is asked only inside a native image, never on Android or a JVM. + executablePath = { ProcessHandle.current().info().command().orElse(null) }, ) /** @@ -53,22 +61,31 @@ internal object PackagedPythonHome { * 3. `<`[COMPOSE_RESOURCES_PROPERTY]`>/`[DIRECTORY_NAME], **only if that directory exists**: * every Compose app has the property, including one whose build did not package a prefix, * and that app must behave exactly as it did before this existed. + * 4. Inside a GraalVM native image only ([NATIVE_IMAGE_CODE_PROPERTY] is `runtime`), + * `/`[DIRECTORY_NAME], **only if that directory exists** + * (issue #246). A native image has no `java.home`, no Compose resources directory and no + * launcher to set `PYTHONHOME`, so its prefix ships beside it, as an iOS app's ships in its + * bundle. On a JVM the executable is `java`, and a directory beside it is the JDK's. */ fun resolve( environmentHome: String?, property: (String) -> String?, isDirectory: (String) -> Boolean, + executablePath: () -> String? = { null }, ): Resolved? { if (!environmentHome.isNullOrBlank()) return null property(HOME_PROPERTY)?.takeIf { it.isNotBlank() }?.let { return Resolved(it, "system property $HOME_PROPERTY") } - val resources = property(COMPOSE_RESOURCES_PROPERTY)?.takeIf { it.isNotBlank() } ?: return null - val candidate = File(resources, DIRECTORY_NAME).path - return if (isDirectory(candidate)) { - Resolved(candidate, "$DIRECTORY_NAME under system property $COMPOSE_RESOURCES_PROPERTY") - } else { - null + property(COMPOSE_RESOURCES_PROPERTY)?.takeIf { it.isNotBlank() }?.let { resources -> + val candidate = File(resources, DIRECTORY_NAME).path + if (isDirectory(candidate)) { + return Resolved(candidate, "$DIRECTORY_NAME under system property $COMPOSE_RESOURCES_PROPERTY") + } } + if (property(NATIVE_IMAGE_CODE_PROPERTY) != "runtime") return null + val executable = executablePath() ?: return null + val candidate = File(File(executable).absoluteFile.parentFile, DIRECTORY_NAME).path + return if (isDirectory(candidate)) Resolved(candidate, "$DIRECTORY_NAME beside the native image $executable") else null } } diff --git a/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/ref/PhantomCleanerRegistry.kt b/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/ref/PhantomCleanerRegistry.kt index e636a9131..848633ff5 100644 --- a/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/ref/PhantomCleanerRegistry.kt +++ b/python-multiplatform/src/jvmMain/kotlin/python/multiplatform/ref/PhantomCleanerRegistry.kt @@ -14,13 +14,13 @@ import java.util.concurrent.atomic.AtomicLong * [ReferenceQueue] the collector enqueues them on, and a thread that drains it. * * **Why this is in `jvmMain` rather than `androidMain`.** It is the fallback for an Android version, - * but nothing in it is Android-specific — `PhantomReference` and `ReferenceQueue` are Java 1.2. It + * but nothing in it is Android-specific, `PhantomReference` and `ReferenceQueue` are Java 1.2. It * used to live in `androidMain`, and the consequence was that ROADMAP §4 could only say "Android * below API 33 uses the `PhantomReference` path and is not covered yet": the only way to run it was * on a device below API 33, and the emulators this project has are API 36, which take the `Cleaner` * branch and never reach here. Sitting in `jvmMain` it is reachable from `desktopTest`, and - * `PhantomCleanerRegistryTest` runs it on every desktop build. `desktopMain` does not use it — it - * has a real `Cleaner` — so this is a test seam, not a change of behaviour for that target. + * `PhantomCleanerRegistryTest` runs it on every desktop build. `desktopMain` does not use it, it + * has a real `Cleaner`, so this is a test seam, not a change of behaviour for that target. * * **The action must not be able to reach its owner.** The registry holds every live entry strongly, * and each entry holds its action strongly; an action that could reach the owner would therefore @@ -40,7 +40,7 @@ internal object PhantomCleanerRegistry { * Every entry that has not been released yet, held strongly. * * A `PhantomReference` nobody holds is itself collectable, and a collected reference is never - * enqueued — so this table is not bookkeeping, it is what makes the mechanism work at all. + * enqueued, so this table is not bookkeeping, it is what makes the mechanism work at all. */ private val active = ConcurrentHashMap() @@ -84,7 +84,7 @@ internal object PhantomCleanerRegistry { * **The `Throwable` catch is the whole point of this loop's shape**, and it was measured before * it was written: with only the `InterruptedException` catch this loop used to have, one * release action that threw propagated out of the loop, out of the `Runnable`, and ended the - * thread — after which *every* later release on the whole process was silently never made. + * thread, after which *every* later release on the whole process was silently never made. * `PhantomCleanerRegistryTest.aReleaseThatThrowsDoesNotStopLaterReleases` failed with * `count: 0`, and so did the unrelated case that happened to run after it. `java.lang.ref * .Cleaner`'s own thread swallows `Throwable` for exactly this reason, which is why the API 33+ @@ -118,7 +118,7 @@ internal object PhantomCleanerRegistry { /** * Arranges for [action] to run once [owner] is unreachable, or once [Cleanable.clean] is - * called — whichever happens first, and never both. + * called, whichever happens first, and never both. */ fun register(owner: Any, action: Runnable): Cleanable { val entry = Entry(owner, action) diff --git a/python-multiplatform/src/jvmMain/kotlin/python/native/ffi/EmbedAPI.jvm.kt b/python-multiplatform/src/jvmMain/kotlin/python/native/ffi/EmbedAPI.jvm.kt index 13ee65f72..c8f9f18d9 100644 --- a/python-multiplatform/src/jvmMain/kotlin/python/native/ffi/EmbedAPI.jvm.kt +++ b/python-multiplatform/src/jvmMain/kotlin/python/native/ffi/EmbedAPI.jvm.kt @@ -2,7 +2,7 @@ package python.native.ffi /** * `NativePointer.address` always carries a boxed `Long` on both JVM leaves (Android's `JNIPointer` - * is a typealias for `Long`), so this cast — and [toRawValue] below — behave identically on + * is a typealias for `Long`), so this cast, and [toRawValue] below, behave identically on * Android and desktop and don't need a platform-specific `actual`. */ inline fun NativePointer.toPlatformPointer(): Long = this.address as Long diff --git a/python-multiplatform/src/jvmTest/kotlin/python/multiplatform/ref/PhantomCleanerRegistryTest.kt b/python-multiplatform/src/jvmTest/kotlin/python/multiplatform/ref/PhantomCleanerRegistryTest.kt index 75ec4212f..76bae7491 100644 --- a/python-multiplatform/src/jvmTest/kotlin/python/multiplatform/ref/PhantomCleanerRegistryTest.kt +++ b/python-multiplatform/src/jvmTest/kotlin/python/multiplatform/ref/PhantomCleanerRegistryTest.kt @@ -9,24 +9,24 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue /** - * The `PhantomReference` release path — ROADMAP §4's one uncovered corner. + * The `PhantomReference` release path, ROADMAP §4's one uncovered corner. * * `java.lang.ref.Cleaner` arrived in **Android API 33**. Every device below that runs a different * mechanism, and §4 has said "Android below API 33 uses the `PhantomReference` path and is not * covered yet" for as long as the section has existed. It stayed uncovered for a structural reason * rather than an oversight: the code lived in `androidMain`, so the only way to run it was on a - * device, and the emulators in this project are API 36 — which takes the `Cleaner` branch and never + * device, and the emulators in this project are API 36, which takes the `Cleaner` branch and never * touches the fallback at all. The fallback could have been arbitrarily broken and every test in * the repository would still have passed. * * [PhantomCleanerRegistry] therefore lives in `jvmMain`, where `desktopTest` can reach it. Nothing - * in it is Android-specific — `java.lang.ref.PhantomReference` and `ReferenceQueue` are Java 1.2 — + * in it is Android-specific, `java.lang.ref.PhantomReference` and `ReferenceQueue` are Java 1.2, * so running it on a desktop JVM exercises the same class an API 26 device would. * * **It runs on both runtimes, and that immediately paid for itself.** This class sits in `jvmTest`, * which `desktopTest` and `androidInstrumentedTest` both depend on, so every case here executes on * HotSpot *and* on ART. The first six-target run after it landed was green on desktop and red on - * both emulators — API 36 with two failures, API 26 with three — for two distinct reasons. One is + * both emulators, API 36 with two failures, API 26 with three, for two distinct reasons. One is * the test's own (`System.gc()` is not a collection on ART; see [awaitCount]) and one is real * (`Reference.reachabilityFence` is API 28+ and the fallback's whole audience is below API 33; see * [keptAlive]). Neither is visible from a desktop-only run, and the second is a genuine API-level @@ -54,7 +54,7 @@ class PhantomCleanerRegistryTest { * * The action closes over [counter] and nothing else. An action that could reach its own owner * would keep that owner strongly reachable through the registry's entry table, and the release - * would never run — which is the single way to write this class that disables it silently. + * would never run, which is the single way to write this class that disables it silently. */ private fun registerAndDrop(counter: AtomicInteger, throwing: Boolean = false): WeakReference { val owner = Owner() @@ -90,7 +90,7 @@ class PhantomCleanerRegistryTest { } } - /** Registers, releases by hand, and drops the owner — all inside one frame, as above. */ + /** Registers, releases by hand, and drops the owner, all inside one frame, as above. */ private fun registerCleanAndDrop(counter: AtomicInteger, cleanTimes: Int = 1) { val owner = Owner() val cleanable = PhantomCleanerRegistry.register(owner, Runnable { counter.incrementAndGet() }) @@ -112,7 +112,7 @@ class PhantomCleanerRegistryTest { * `System.gc()` body back and running this class alone on API 36 reproduced it directly: * `owners collected: 0/2; drainThread alive: true; registry drained: 1, failed: 1` for * [twoDroppedOwnersRunTwoReleases], and `owners collected: 0/1` for the other two waiting cases. - * Twenty seconds of `System.gc()` had reclaimed *nothing* — while the drain thread was alive the + * Twenty seconds of `System.gc()` had reclaimed *nothing*, while the drain thread was alive the * whole time, and every entry the collector genuinely did hand over (`drained: 1`, plus the * throwing one at `failed: 1`) was delivered correctly. The registry was never the fault; the * waiting was. diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/Python.h b/python-multiplatform/src/nativeInterop/cinterop/include/Python.h index db8165e4b..337119c15 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/Python.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/Python.h @@ -9,10 +9,11 @@ // is not needed. -// Include Python header files -#include "patchlevel.h" -#include "pyconfig.h" -#include "pymacconfig.h" +// Include Python configuration headers +#include "patchlevel.h" // the Python version +#include "pyconfig.h" // information from configure +#include "pymacconfig.h" // overrides for pyconfig +#include "pyabi.h" // feature/ABI selection // Include standard header files @@ -22,12 +23,13 @@ #include // INT_MAX #include // HUGE_VAL #include // va_list +#include // memcpy() #include // wchar_t #ifdef HAVE_SYS_TYPES_H # include // ssize_t #endif -// , , and headers are no longer used +// , and headers are no longer used // by Python, but kept for the backward compatibility of existing third party C // extensions. They are not included by limited C API version 3.11 and newer. // @@ -37,7 +39,6 @@ # include // errno # include // FILE* # include // getenv() -# include // memcpy() #endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030d0000 # include // tolower() @@ -46,30 +47,26 @@ # endif #endif -// gh-111506: The free-threaded build is not compatible with the limited API -// or the stable ABI. -#if defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED) -# error "The limited API is not currently supported in the free-threaded build" -#endif - -#if defined(Py_GIL_DISABLED) && defined(_MSC_VER) -# include // __readgsqword() -#endif +#if !defined(Py_LIMITED_API) +# if defined(Py_GIL_DISABLED) +# if defined(_MSC_VER) || defined(__MINGW32__) +# include // __readgsqword() +# endif +# endif +#endif // Py_GIL_DISABLED -#if defined(Py_GIL_DISABLED) && defined(__MINGW32__) -# include // __readgsqword() +#ifdef _MSC_VER +// Ignore MSC warning C4201: "nonstandard extension used: nameless +// struct/union". (Only generated for C standard versions less than C11, which +// we don't *officially* support.) +__pragma(warning(push)) +__pragma(warning(disable: 4201)) #endif -// Suppress known warnings in Python header files. -#if defined(_MSC_VER) -// Warning that alignas behaviour has changed. Doesn't affect us, because we -// never relied on the old behaviour. -#pragma warning(push) -#pragma warning(disable: 5274) -#endif // Include Python header files #include "pyport.h" +#include "exports.h" #include "pymacro.h" #include "pymath.h" #include "pymem.h" @@ -77,12 +74,13 @@ #include "pybuffer.h" #include "pystats.h" #include "pyatomic.h" -#include "lock.h" +#include "cpython/pylock.h" #include "critical_section.h" #include "object.h" #include "refcount.h" #include "objimpl.h" -#include "typeslots.h" +#include "slots.h" +#include "slots_generated.h" #include "pyhash.h" #include "cpython/pydebug.h" #include "bytearrayobject.h" @@ -104,7 +102,7 @@ #include "setobject.h" #include "methodobject.h" #include "moduleobject.h" -#include "monitoring.h" +#include "cpython/monitoring.h" #include "cpython/funcobject.h" #include "cpython/classobject.h" #include "fileobject.h" @@ -120,6 +118,7 @@ #include "cpython/genobject.h" #include "descrobject.h" #include "genericaliasobject.h" +#include "cpython/sentinelobject.h" #include "warnings.h" #include "weakrefobject.h" #include "structseq.h" @@ -147,9 +146,8 @@ #include "cpython/pyfpe.h" #include "cpython/tracemalloc.h" -// Restore warning filter #ifdef _MSC_VER -#pragma warning(pop) +__pragma(warning(pop)) // warning(disable: 4201) #endif #endif /* !Py_PYTHON_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/abstract.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/abstract.h index ffd19ccd3..7490ece52 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/abstract.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/abstract.h @@ -6,7 +6,7 @@ /* Like PyObject_CallMethod(), but expect a _Py_Identifier* as the method name. */ -PyAPI_FUNC(PyObject*) _PyObject_CallMethodId( +Py_DEPRECATED(3.15) PyAPI_FUNC(PyObject*) _PyObject_CallMethodId( PyObject *obj, _Py_Identifier *name, const char *format, ...); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytearrayobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytearrayobject.h index 4dddef713..1edd08207 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytearrayobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytearrayobject.h @@ -5,25 +5,25 @@ /* Object layout */ typedef struct { PyObject_VAR_HEAD - Py_ssize_t ob_alloc; /* How many bytes allocated in ob_bytes */ + /* How many bytes allocated in ob_bytes + + In the current implementation this is equivalent to Py_SIZE(ob_bytes_object). + The value is always loaded and stored atomically for thread safety. + There are API compatibilty concerns with removing so keeping for now. */ + Py_ssize_t ob_alloc; char *ob_bytes; /* Physical backing buffer */ char *ob_start; /* Logical start inside ob_bytes */ Py_ssize_t ob_exports; /* How many buffer exports */ + PyObject *ob_bytes_object; /* PyBytes for zero-copy bytes conversion */ } PyByteArrayObject; -PyAPI_DATA(char) _PyByteArray_empty_string[]; - /* Macros and static inline functions, trading safety for speed */ #define _PyByteArray_CAST(op) \ (assert(PyByteArray_Check(op)), _Py_CAST(PyByteArrayObject*, op)) static inline char* PyByteArray_AS_STRING(PyObject *op) { - PyByteArrayObject *self = _PyByteArray_CAST(op); - if (Py_SIZE(self)) { - return self->ob_start; - } - return _PyByteArray_empty_string; + return _PyByteArray_CAST(op)->ob_start; } #define PyByteArray_AS_STRING(self) PyByteArray_AS_STRING(_PyObject_CAST(self)) diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytesobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytesobject.h index 71c133f17..85bc2b827 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytesobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/bytesobject.h @@ -40,3 +40,46 @@ _PyBytes_Join(PyObject *sep, PyObject *iterable) { return PyBytes_Join(sep, iterable); } + + +// --- PyBytesWriter API ----------------------------------------------------- + +typedef struct PyBytesWriter PyBytesWriter; + +PyAPI_FUNC(PyBytesWriter *) PyBytesWriter_Create( + Py_ssize_t size); +PyAPI_FUNC(void) PyBytesWriter_Discard( + PyBytesWriter *writer); +PyAPI_FUNC(PyObject*) PyBytesWriter_Finish( + PyBytesWriter *writer); +PyAPI_FUNC(PyObject*) PyBytesWriter_FinishWithSize( + PyBytesWriter *writer, + Py_ssize_t size); +PyAPI_FUNC(PyObject*) PyBytesWriter_FinishWithPointer( + PyBytesWriter *writer, + void *buf); + +PyAPI_FUNC(void*) PyBytesWriter_GetData( + PyBytesWriter *writer); +PyAPI_FUNC(Py_ssize_t) PyBytesWriter_GetSize( + PyBytesWriter *writer); + +PyAPI_FUNC(int) PyBytesWriter_WriteBytes( + PyBytesWriter *writer, + const void *bytes, + Py_ssize_t size); +PyAPI_FUNC(int) PyBytesWriter_Format( + PyBytesWriter *writer, + const char *format, + ...); + +PyAPI_FUNC(int) PyBytesWriter_Resize( + PyBytesWriter *writer, + Py_ssize_t size); +PyAPI_FUNC(int) PyBytesWriter_Grow( + PyBytesWriter *writer, + Py_ssize_t size); +PyAPI_FUNC(void*) PyBytesWriter_GrowAndUpdatePointer( + PyBytesWriter *writer, + Py_ssize_t size, + void *buf); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/ceval.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/ceval.h index ca8109e32..5b66fa104 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/ceval.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/ceval.h @@ -23,6 +23,9 @@ _PyEval_RequestCodeExtraIndex(freefunc f) { PyAPI_FUNC(int) _PyEval_SliceIndex(PyObject *, Py_ssize_t *); PyAPI_FUNC(int) _PyEval_SliceIndexNotNone(PyObject *, Py_ssize_t *); +PyAPI_FUNC(int) _PyEval_UnpackIndices(PyObject *, PyObject *, + Py_ssize_t, + Py_ssize_t *, Py_ssize_t *); // Trampoline API @@ -35,7 +38,7 @@ typedef struct { PyAPI_FUNC(int) PyUnstable_PerfMapState_Init(void); PyAPI_FUNC(int) PyUnstable_WritePerfMapEntry( const void *code_addr, - unsigned int code_size, + size_t code_size, const char *entry_name); PyAPI_FUNC(void) PyUnstable_PerfMapState_Fini(void); PyAPI_FUNC(int) PyUnstable_CopyPerfMapFile(const char* parent_filename); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/code.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/code.h index 3f0dce034..84456a709 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/code.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/code.h @@ -282,15 +282,6 @@ typedef struct _line_offsets { */ PyAPI_FUNC(int) _PyCode_CheckLineNumber(int lasti, PyCodeAddressRange *bounds); -/* Create a comparable key used to compare constants taking in account the - * object type. It is used to make sure types are not coerced (e.g., float and - * complex) _and_ to distinguish 0.0 from -0.0 e.g. on IEEE platforms - * - * Return (type(obj), obj, ...): a tuple with variable size (at least 2 items) - * depending on the type and the value. The type is the first item to not - * compare bytes and str which can raise a BytesWarning exception. */ -PyAPI_FUNC(PyObject*) _PyCode_ConstantKey(PyObject *obj); - PyAPI_FUNC(PyObject*) PyCode_Optimize(PyObject *code, PyObject* consts, PyObject *names, PyObject *lnotab); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/complexobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/complexobject.h index fbdc6a91f..58da80140 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/complexobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/complexobject.h @@ -7,7 +7,8 @@ typedef struct { double imag; } Py_complex; -// Operations on complex numbers. +/* Operations on complex numbers (soft deprecated + since Python 3.15). */ PyAPI_FUNC(Py_complex) _Py_c_sum(Py_complex, Py_complex); PyAPI_FUNC(Py_complex) _Py_c_diff(Py_complex, Py_complex); PyAPI_FUNC(Py_complex) _Py_c_neg(Py_complex); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/critical_section.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/critical_section.h index 4fc46fefb..bcba32da4 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/critical_section.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/critical_section.h @@ -2,15 +2,6 @@ # error "this header file must not be included directly" #endif -// Python critical sections -// -// Conceptually, critical sections are a deadlock avoidance layer on top of -// per-object locks. These helpers, in combination with those locks, replace -// our usage of the global interpreter lock to provide thread-safety for -// otherwise thread-unsafe objects, such as dict. -// -// NOTE: These APIs are no-ops in non-free-threaded builds. -// // Straightforward per-object locking could introduce deadlocks that were not // present when running with the GIL. Threads may hold locks for multiple // objects simultaneously because Python operations can nest. If threads were @@ -43,52 +34,19 @@ // `_PyThreadState_Attach()`, it resumes the top-most (i.e., most recent) // critical section by reacquiring the associated lock or locks. See // `_PyCriticalSection_Resume()`. -// -// NOTE: Only the top-most critical section is guaranteed to be active. -// Operations that need to lock two objects at once must use -// `Py_BEGIN_CRITICAL_SECTION2()`. You *CANNOT* use nested critical sections -// to lock more than one object at once, because the inner critical section -// may suspend the outer critical sections. This API does not provide a way -// to lock more than two objects at once (though it could be added later -// if actually needed). -// -// NOTE: Critical sections implicitly behave like reentrant locks because -// attempting to acquire the same lock will suspend any outer (earlier) -// critical sections. However, they are less efficient for this use case than -// purposefully designed reentrant locks. -// -// Example usage: -// Py_BEGIN_CRITICAL_SECTION(op); -// ... -// Py_END_CRITICAL_SECTION(); -// -// To lock two objects at once: -// Py_BEGIN_CRITICAL_SECTION2(op1, op2); -// ... -// Py_END_CRITICAL_SECTION2(); - -typedef struct PyCriticalSection PyCriticalSection; -typedef struct PyCriticalSection2 PyCriticalSection2; - -PyAPI_FUNC(void) -PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op); PyAPI_FUNC(void) PyCriticalSection_BeginMutex(PyCriticalSection *c, PyMutex *m); -PyAPI_FUNC(void) -PyCriticalSection_End(PyCriticalSection *c); - -PyAPI_FUNC(void) -PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b); - PyAPI_FUNC(void) PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2); -PyAPI_FUNC(void) -PyCriticalSection2_End(PyCriticalSection2 *c); - #ifndef Py_GIL_DISABLED +#undef Py_BEGIN_CRITICAL_SECTION +#undef Py_END_CRITICAL_SECTION +#undef Py_BEGIN_CRITICAL_SECTION2 +#undef Py_END_CRITICAL_SECTION2 + # define Py_BEGIN_CRITICAL_SECTION(op) \ { # define Py_BEGIN_CRITICAL_SECTION_MUTEX(mutex) \ @@ -101,54 +59,17 @@ PyCriticalSection2_End(PyCriticalSection2 *c); { # define Py_END_CRITICAL_SECTION2() \ } -#else /* !Py_GIL_DISABLED */ - -// NOTE: the contents of this struct are private and may change betweeen -// Python releases without a deprecation period. -struct PyCriticalSection { - // Tagged pointer to an outer active critical section (or 0). - uintptr_t _cs_prev; - - // Mutex used to protect critical section - PyMutex *_cs_mutex; -}; - -// A critical section protected by two mutexes. Use -// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2. -// NOTE: the contents of this struct are private and may change betweeen -// Python releases without a deprecation period. -struct PyCriticalSection2 { - PyCriticalSection _cs_base; - PyMutex *_cs_mutex2; -}; - -# define Py_BEGIN_CRITICAL_SECTION(op) \ - { \ - PyCriticalSection _py_cs; \ - PyCriticalSection_Begin(&_py_cs, _PyObject_CAST(op)) +#else /* !Py_GIL_DISABLED */ # define Py_BEGIN_CRITICAL_SECTION_MUTEX(mutex) \ { \ PyCriticalSection _py_cs; \ PyCriticalSection_BeginMutex(&_py_cs, mutex) -# define Py_END_CRITICAL_SECTION() \ - PyCriticalSection_End(&_py_cs); \ - } - -# define Py_BEGIN_CRITICAL_SECTION2(a, b) \ - { \ - PyCriticalSection2 _py_cs2; \ - PyCriticalSection2_Begin(&_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b)) - # define Py_BEGIN_CRITICAL_SECTION2_MUTEX(m1, m2) \ { \ PyCriticalSection2 _py_cs2; \ PyCriticalSection2_BeginMutex(&_py_cs2, m1, m2) -# define Py_END_CRITICAL_SECTION2() \ - PyCriticalSection2_End(&_py_cs2); \ - } - -#endif +#endif /* !Py_GIL_DISABLED */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/dictobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/dictobject.h index df9ec7050..5e7811416 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/dictobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/dictobject.h @@ -32,6 +32,16 @@ typedef struct { PyDictValues *ma_values; } PyDictObject; +// frozendict +PyAPI_DATA(PyTypeObject) PyFrozenDict_Type; +#define PyFrozenDict_Check(op) PyObject_TypeCheck((op), &PyFrozenDict_Type) +#define PyFrozenDict_CheckExact(op) Py_IS_TYPE((op), &PyFrozenDict_Type) + +#define PyAnyDict_CheckExact(ob) \ + (PyDict_CheckExact(ob) || PyFrozenDict_CheckExact(ob)) +#define PyAnyDict_Check(ob) \ + (PyDict_Check(ob) || PyFrozenDict_Check(ob)) + PyAPI_FUNC(PyObject *) _PyDict_GetItem_KnownHash(PyObject *mp, PyObject *key, Py_hash_t hash); // PyDict_GetItemStringRef() can be used instead @@ -39,20 +49,10 @@ Py_DEPRECATED(3.14) PyAPI_FUNC(PyObject *) _PyDict_GetItemStringWithError(PyObje PyAPI_FUNC(PyObject *) PyDict_SetDefault( PyObject *mp, PyObject *key, PyObject *defaultobj); -// Inserts `key` with a value `default_value`, if `key` is not already present -// in the dictionary. If `result` is not NULL, then the value associated -// with `key` is returned in `*result` (either the existing value, or the now -// inserted `default_value`). -// Returns: -// -1 on error -// 0 if `key` was not present and `default_value` was inserted -// 1 if `key` was present and `default_value` was not inserted -PyAPI_FUNC(int) PyDict_SetDefaultRef(PyObject *mp, PyObject *key, PyObject *default_value, PyObject **result); - /* Get the number of items of a dictionary. */ static inline Py_ssize_t PyDict_GET_SIZE(PyObject *op) { PyDictObject *mp; - assert(PyDict_Check(op)); + assert(PyAnyDict_Check(op)); mp = _Py_CAST(PyDictObject*, op); #ifdef Py_GIL_DISABLED return _Py_atomic_load_ssize_relaxed(&mp->ma_used); @@ -103,3 +103,6 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id); // Mark given dictionary as "watched" (callback will be called if it is modified) PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict); PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict); + +// Create a frozendict. Create an empty dictionary if iterable is NULL. +PyAPI_FUNC(PyObject*) PyFrozenDict_New(PyObject *iterable); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/funcobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/funcobject.h index 598cd330b..9e1599a76 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/funcobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/funcobject.h @@ -134,7 +134,8 @@ PyAPI_FUNC(PyObject *) PyStaticMethod_New(PyObject *); V(DESTROY) \ V(MODIFY_CODE) \ V(MODIFY_DEFAULTS) \ - V(MODIFY_KWDEFAULTS) + V(MODIFY_KWDEFAULTS) \ + V(MODIFY_QUALNAME) typedef enum { #define PY_DEF_EVENT(EVENT) PyFunction_EVENT_##EVENT, diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/import.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/import.h index 0ce0b1ee6..149a20af8 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/import.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/import.h @@ -10,6 +10,13 @@ struct _inittab { PyAPI_DATA(struct _inittab *) PyImport_Inittab; PyAPI_FUNC(int) PyImport_ExtendInittab(struct _inittab *newtab); +// Custom importers may use this API to initialize statically linked +// extension modules directly from a spec and init function, +// without needing to go through inittab +PyAPI_FUNC(PyObject *) PyImport_CreateModuleFromInitfunc( + PyObject *spec, + PyObject *(*initfunc)(void)); + struct _frozen { const char *name; /* ASCII encoded string */ const unsigned char *code; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/initconfig.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/initconfig.h index 7ce4acfeb..1ccc496c6 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/initconfig.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/initconfig.h @@ -102,15 +102,14 @@ typedef struct PyPreConfig { /* Enable UTF-8 mode? (PEP 540) - Disabled by default (equals to 0). + If equal to 1, use the UTF-8 encoding and use "surrogateescape" for the + stdin & stdout error handlers. - Set to 1 by "-X utf8" and "-X utf8=1" command line options. - Set to 1 by PYTHONUTF8=1 environment variable. + Enabled by default (equal to 1; PEP 686), or if Py_UTF8Mode=1, + or if "-X utf8=1" or PYTHONUTF8=1. - Set to 0 by "-X utf8=0" and PYTHONUTF8=0. - - If equals to -1, it is set to 1 if the LC_CTYPE locale is "C" or - "POSIX", otherwise it is set to 0. Inherit Py_UTF8Mode value value. */ + Set to 0 by "-X utf8=0" or PYTHONUTF8=0. + */ int utf8_mode; /* If non-zero, enable the Python Development Mode. @@ -150,6 +149,7 @@ typedef struct PyConfig { int dump_refs; wchar_t *dump_refs_file; int malloc_stats; + int pymalloc_hugepages; wchar_t *filesystem_encoding; wchar_t *filesystem_errors; wchar_t *pycache_prefix; @@ -191,6 +191,7 @@ typedef struct PyConfig { int enable_gil; int tlbc_enabled; #endif + int lazy_imports; /* --- Path configuration inputs ------------ */ int pathconfig_warnings; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/longintrepr.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/longintrepr.h index 4b6f97a5e..998ebe689 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/longintrepr.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/longintrepr.h @@ -133,50 +133,16 @@ _PyLong_CompactValue(const PyLongObject *op) assert(PyType_HasFeature(op->ob_base.ob_type, Py_TPFLAGS_LONG_SUBCLASS)); assert(PyUnstable_Long_IsCompact(op)); sign = 1 - (op->long_value.lv_tag & _PyLong_SIGN_MASK); + if (sign == 0) { + // gh-147988: Make sure that the digit is zero. + // It helps detecting the usage of uninitialized digits. + assert(op->long_value.ob_digit[0] == 0); + } return sign * (Py_ssize_t)op->long_value.ob_digit[0]; } #define PyUnstable_Long_CompactValue _PyLong_CompactValue - -/* --- Import/Export API -------------------------------------------------- */ - -typedef struct PyLongLayout { - uint8_t bits_per_digit; - uint8_t digit_size; - int8_t digits_order; - int8_t digit_endianness; -} PyLongLayout; - -PyAPI_FUNC(const PyLongLayout*) PyLong_GetNativeLayout(void); - -typedef struct PyLongExport { - int64_t value; - uint8_t negative; - Py_ssize_t ndigits; - const void *digits; - // Member used internally, must not be used for other purpose. - Py_uintptr_t _reserved; -} PyLongExport; - -PyAPI_FUNC(int) PyLong_Export( - PyObject *obj, - PyLongExport *export_long); -PyAPI_FUNC(void) PyLong_FreeExport( - PyLongExport *export_long); - - -/* --- PyLongWriter API --------------------------------------------------- */ - -typedef struct PyLongWriter PyLongWriter; - -PyAPI_FUNC(PyLongWriter*) PyLongWriter_Create( - int negative, - Py_ssize_t ndigits, - void **digits); -PyAPI_FUNC(PyObject*) PyLongWriter_Finish(PyLongWriter *writer); -PyAPI_FUNC(void) PyLongWriter_Discard(PyLongWriter *writer); - #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/marshal.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/marshal.h new file mode 100644 index 000000000..159459fca --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/marshal.h @@ -0,0 +1,17 @@ +#ifndef _Py_CPYTHON_MARSHAL_H +# error "this header file must not be included directly" +#endif + +PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromString(const char *, + Py_ssize_t); +PyAPI_FUNC(PyObject *) PyMarshal_WriteObjectToString(PyObject *, int); + +#define Py_MARSHAL_VERSION 6 + +PyAPI_FUNC(long) PyMarshal_ReadLongFromFile(FILE *); +PyAPI_FUNC(int) PyMarshal_ReadShortFromFile(FILE *); +PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromFile(FILE *); +PyAPI_FUNC(PyObject *) PyMarshal_ReadLastObjectFromFile(FILE *); + +PyAPI_FUNC(void) PyMarshal_WriteLongToFile(long, FILE *, int); +PyAPI_FUNC(void) PyMarshal_WriteObjectToFile(PyObject *, FILE *, int); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/modsupport.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/modsupport.h index d3b88f58c..cfeee6e8a 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/modsupport.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/modsupport.h @@ -2,6 +2,19 @@ # error "this header file must not be included directly" #endif +PyAPI_FUNC(int) PyArg_ParseArray( + PyObject *const *args, + Py_ssize_t nargs, + const char *format, + ...); +PyAPI_FUNC(int) PyArg_ParseArrayAndKeywords( + PyObject *const *args, + Py_ssize_t nargs, + PyObject *kwnames, + const char *format, + const char * const *kwlist, + ...); + // A data structure that can be used to run initialization code once in a // thread-safe manner. The C++11 equivalent is std::call_once. typedef struct { @@ -24,3 +37,9 @@ typedef struct _PyArg_Parser { PyAPI_FUNC(int) _PyArg_ParseTupleAndKeywordsFast(PyObject *, PyObject *, struct _PyArg_Parser *, ...); + +#ifdef Py_BUILD_CORE +// For internal use in stdlib. Needs C99 compound literals. +// Defined here to avoid every stdlib module including pycore_modsupport.h +#define _Py_ABI_SLOT {Py_mod_abi, (void*) &(PyABIInfo) _PyABIInfo_DEFAULT} +#endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/monitoring.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/monitoring.h index ce9294240..c93271f6c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/monitoring.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/monitoring.h @@ -1,7 +1,13 @@ -#ifndef Py_CPYTHON_MONITORING_H -# error "this header file must not be included directly" +#ifndef Py_MONITORING_H +#define Py_MONITORING_H +#ifndef Py_LIMITED_API +#ifdef __cplusplus +extern "C" { #endif +// There is currently no limited API for monitoring + + /* Local events. * These require bytecode instrumentation */ @@ -18,9 +24,10 @@ #define PY_MONITORING_EVENT_STOP_ITERATION 10 #define PY_MONITORING_IS_INSTRUMENTED_EVENT(ev) \ - ((ev) < _PY_MONITORING_LOCAL_EVENTS) +((ev) <= PY_MONITORING_EVENT_STOP_ITERATION) -/* Other events, mainly exceptions */ +/* Other events, mainly exceptions. + * These can now be turned on and disabled on a per code object basis. */ #define PY_MONITORING_EVENT_RAISE 11 #define PY_MONITORING_EVENT_EXCEPTION_HANDLED 12 @@ -28,6 +35,9 @@ #define PY_MONITORING_EVENT_PY_THROW 14 #define PY_MONITORING_EVENT_RERAISE 15 +#define _PY_MONITORING_IS_UNGROUPED_EVENT(ev) \ +((ev) < _PY_MONITORING_UNGROUPED_EVENTS) + /* Ancillary events */ @@ -267,3 +277,9 @@ PyMonitoring_FireStopIterationEvent(PyMonitoringState *state, PyObject *codelike } #undef _PYMONITORING_IF_ACTIVE + +#ifdef __cplusplus +} +#endif +#endif // !Py_LIMITED_API +#endif // !Py_MONITORING_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/object.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/object.h index 973d358ed..c8b8119d4 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/object.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/object.h @@ -230,6 +230,8 @@ struct _typeobject { destructor tp_finalize; vectorcallfunc tp_vectorcall; + /* Below here all fields are internal to the VM */ + /* bitset of which type-watchers care about this type */ unsigned char tp_watched; @@ -239,6 +241,11 @@ struct _typeobject { * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere). */ uint16_t tp_versions_used; + + /* Virtual iterator next function. + * This function must escape to any code that can result in + * the GC being run, such as Py_DECREF. */ + _Py_iteritemfunc _tp_iteritem; }; #define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used) @@ -295,13 +302,15 @@ PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *); PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int); PyAPI_FUNC(void) _Py_BreakPoint(void); -PyAPI_FUNC(void) _PyObject_Dump(PyObject *); +PyAPI_FUNC(void) PyObject_Dump(PyObject *); + +// Alias for backward compatibility +#define _PyObject_Dump PyObject_Dump -PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *); +Py_DEPRECATED(3.15) PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *); PyAPI_FUNC(PyObject **) _PyObject_GetDictPtr(PyObject *); PyAPI_FUNC(void) PyObject_CallFinalizer(PyObject *); -PyAPI_FUNC(int) PyObject_CallFinalizerFromDealloc(PyObject *); PyAPI_FUNC(void) PyUnstable_Object_ClearWeakRefsNoCallbacks(PyObject *); @@ -387,10 +396,11 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *); process with a message on stderr if the given condition fails to hold, but compile away to nothing if NDEBUG is defined. - However, before aborting, Python will also try to call _PyObject_Dump() on - the given object. This may be of use when investigating bugs in which a - particular object is corrupt (e.g. buggy a tp_visit method in an extension - module breaking the garbage collector), to help locate the broken objects. + However, before aborting, Python will also try to call + PyObject_Dump() on the given object. This may be of use when + investigating bugs in which a particular object is corrupt (e.g. buggy a + tp_visit method in an extension module breaking the garbage collector), to + help locate the broken objects. The WITH_MSG variant allows you to supply an additional message that Python will attempt to print to stderr, after the object dump. */ @@ -432,17 +442,15 @@ PyAPI_FUNC(void) _Py_NO_RETURN _PyObject_AssertFailed( PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op); PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(PyThreadState *tstate); -PyAPI_FUNC(int) _Py_ReachedRecursionLimitWithMargin(PyThreadState *tstate, int margin_count); - /* For backwards compatibility with the old trashcan mechanism */ #define Py_TRASHCAN_BEGIN(op, dealloc) #define Py_TRASHCAN_END PyAPI_FUNC(void *) PyObject_GetItemData(PyObject *obj); +PyAPI_FUNC(void *) PyObject_GetItemData_DuringGC(PyObject *obj); PyAPI_FUNC(int) PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg); -PyAPI_FUNC(int) _PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict); PyAPI_FUNC(void) PyObject_ClearManagedDict(PyObject *obj); @@ -463,6 +471,7 @@ PyAPI_FUNC(int) PyUnstable_Type_AssignVersionTag(PyTypeObject *type); typedef enum { PyRefTracer_CREATE = 0, PyRefTracer_DESTROY = 1, + PyRefTracer_TRACKER_REMOVED = 2, } PyRefTracerEvent; typedef int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void *); @@ -491,3 +500,85 @@ PyAPI_FUNC(int) PyUnstable_TryIncRef(PyObject *); PyAPI_FUNC(void) PyUnstable_EnableTryIncRef(PyObject *); PyAPI_FUNC(int) PyUnstable_Object_IsUniquelyReferenced(PyObject *); + +PyAPI_FUNC(int) PyUnstable_SetImmortal(PyObject *op); + +#if defined(Py_GIL_DISABLED) +PyAPI_FUNC(uintptr_t) _Py_GetThreadLocal_Addr(void); + +static inline uintptr_t +_Py_ThreadId(void) +{ + uintptr_t tid; +#if defined(_MSC_VER) && defined(_M_X64) + tid = __readgsqword(48); +#elif defined(_MSC_VER) && defined(_M_IX86) + tid = __readfsdword(24); +#elif defined(_MSC_VER) && defined(_M_ARM64) + tid = __getReg(18); +#elif defined(__MINGW32__) && defined(_M_X64) + tid = __readgsqword(48); +#elif defined(__MINGW32__) && defined(_M_IX86) + tid = __readfsdword(24); +#elif defined(__MINGW32__) && defined(_M_ARM64) + // x18 is the Windows ARM64 platform register and points to the TEB. + __asm__ ("mov %0, x18" : "=r" (tid)); +#elif defined(__i386__) + __asm__("{movl %%gs:0, %0|mov %0, dword ptr gs:[0]}" : "=r" (tid)); // 32-bit always uses GS +#elif defined(__MACH__) && defined(__x86_64__) + __asm__("{movq %%gs:0, %0|mov %0, qword ptr gs:[0]}" : "=r" (tid)); // x86_64 macOSX uses GS +#elif defined(__x86_64__) + __asm__("{movq %%fs:0, %0|mov %0, qword ptr fs:[0]}" : "=r" (tid)); // x86_64 Linux, BSD uses FS +#elif defined(__arm__) && __ARM_ARCH >= 7 + __asm__ ("mrc p15, 0, %0, c13, c0, 3\nbic %0, %0, #3" : "=r" (tid)); +#elif defined(__aarch64__) && defined(__APPLE__) + __asm__ ("mrs %0, tpidrro_el0" : "=r" (tid)); +#elif defined(__aarch64__) + __asm__ ("mrs %0, tpidr_el0" : "=r" (tid)); +#elif defined(__powerpc64__) + #if defined(__clang__) && _Py__has_builtin(__builtin_thread_pointer) + tid = (uintptr_t)__builtin_thread_pointer(); + #else + // r13 is reserved for use as system thread ID by the Power 64-bit ABI. + register uintptr_t tp __asm__ ("r13"); + __asm__("" : "=r" (tp)); + tid = tp; + #endif +#elif defined(__powerpc__) + #if defined(__clang__) && _Py__has_builtin(__builtin_thread_pointer) + tid = (uintptr_t)__builtin_thread_pointer(); + #else + // r2 is reserved for use as system thread ID by the Power 32-bit ABI. + register uintptr_t tp __asm__ ("r2"); + __asm__ ("" : "=r" (tp)); + tid = tp; + #endif +#elif defined(__s390__) && defined(__GNUC__) + // Both GCC and Clang have supported __builtin_thread_pointer + // for s390 from long time ago. + tid = (uintptr_t)__builtin_thread_pointer(); +#elif defined(__riscv) + #if defined(__clang__) && _Py__has_builtin(__builtin_thread_pointer) + tid = (uintptr_t)__builtin_thread_pointer(); + #else + // tp is Thread Pointer provided by the RISC-V ABI. + __asm__ ("mv %0, tp" : "=r" (tid)); + #endif +#else + // Fallback to a portable implementation if we do not have a faster + // platform-specific implementation. + tid = _Py_GetThreadLocal_Addr(); +#endif + return tid; +} + +static inline Py_ALWAYS_INLINE int +_Py_IsOwnedByCurrentThread(PyObject *ob) +{ +#ifdef _Py_THREAD_SANITIZER + return _Py_atomic_load_uintptr_relaxed(&ob->ob_tid) == _Py_ThreadId(); +#else + return ob->ob_tid == _Py_ThreadId(); +#endif +} +#endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic.h index 3a71ed689..e85b360c9 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic.h @@ -523,6 +523,9 @@ _Py_atomic_store_uintptr_release(uintptr_t *obj, uintptr_t value); static inline void _Py_atomic_store_ssize_release(Py_ssize_t *obj, Py_ssize_t value); +static inline void +_Py_atomic_store_int8_release(int8_t *obj, int8_t value); + static inline void _Py_atomic_store_int_release(int *obj, int value); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_gcc.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_gcc.h index 465226a76..253b35082 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_gcc.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_gcc.h @@ -572,6 +572,10 @@ static inline void _Py_atomic_store_int_release(int *obj, int value) { __atomic_store_n(obj, value, __ATOMIC_RELEASE); } +static inline void +_Py_atomic_store_int8_release(int8_t *obj, int8_t value) +{ __atomic_store_n(obj, value, __ATOMIC_RELEASE); } + static inline void _Py_atomic_store_ssize_release(Py_ssize_t *obj, Py_ssize_t value) { __atomic_store_n(obj, value, __ATOMIC_RELEASE); } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_msc.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_msc.h index eb6df819f..3b3c5f701 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_msc.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_msc.h @@ -1060,6 +1060,19 @@ _Py_atomic_store_int_release(int *obj, int value) #endif } +static inline void +_Py_atomic_store_int8_release(int8_t *obj, int8_t value) +{ +#if defined(_M_X64) || defined(_M_IX86) + *(int8_t volatile *)obj = value; +#elif defined(_M_ARM64) + _Py_atomic_ASSERT_ARG_TYPE(unsigned __int8); + __stlr8((unsigned __int8 volatile *)obj, (unsigned __int8)value); +#else +# error "no implementation of _Py_atomic_store_int8_release" +#endif +} + static inline void _Py_atomic_store_uint_release(unsigned int *obj, unsigned int value) { diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_std.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_std.h index 1a2ccd0cc..faef303da 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_std.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyatomic_std.h @@ -1023,6 +1023,14 @@ _Py_atomic_store_int_release(int *obj, int value) memory_order_release); } +static inline void +_Py_atomic_store_int8_release(int8_t *obj, int8_t value) +{ + _Py_USING_STD; + atomic_store_explicit((_Atomic(int8_t)*)obj, value, + memory_order_release); +} + static inline void _Py_atomic_store_uint_release(unsigned int *obj, unsigned int value) { diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyhash.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyhash.h index a33ba10b8..dac223368 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyhash.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pyhash.h @@ -7,7 +7,7 @@ /* Parameters used for the numeric hash implementation. See notes for _Py_HashDouble in Python/pyhash.c. Numeric hashes are based on - reduction modulo the prime 2**_PyHASH_BITS - 1. */ + reduction modulo the prime 2**PyHASH_BITS - 1. */ #if SIZEOF_VOID_P >= 8 # define PyHASH_BITS 61 @@ -15,7 +15,7 @@ # define PyHASH_BITS 31 #endif -#define PyHASH_MODULUS (((size_t)1 << _PyHASH_BITS) - 1) +#define PyHASH_MODULUS (((size_t)1 << PyHASH_BITS) - 1) #define PyHASH_INF 314159 #define PyHASH_IMAG PyHASH_MULTIPLIER diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pylifecycle.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pylifecycle.h index 86ce6e6f7..e46dfe59e 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pylifecycle.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pylifecycle.h @@ -25,6 +25,9 @@ PyAPI_FUNC(PyStatus) Py_PreInitializeFromArgs( PyAPI_FUNC(PyStatus) Py_InitializeFromConfig( const PyConfig *config); +// Python 3.8 provisional API (PEP 587) +PyAPI_FUNC(PyStatus) _Py_InitializeMain(void); + PyAPI_FUNC(int) Py_RunMain(void); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/lock.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pylock.h similarity index 92% rename from python-multiplatform/src/nativeInterop/cinterop/include/cpython/lock.h rename to python-multiplatform/src/nativeInterop/cinterop/include/cpython/pylock.h index 63886fca2..460ac2c9f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/lock.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pylock.h @@ -1,7 +1,11 @@ -#ifndef Py_CPYTHON_LOCK_H -# error "this header file must not be included directly" +#ifndef Py_LOCK_H +#define Py_LOCK_H +#ifndef Py_LIMITED_API +#ifdef __cplusplus +extern "C" { #endif + #define _Py_UNLOCKED 0 #define _Py_LOCKED 1 @@ -72,3 +76,10 @@ _PyMutex_IsLocked(PyMutex *m) return (_Py_atomic_load_uint8(&m->_bits) & _Py_LOCKED) != 0; } #define PyMutex_IsLocked _PyMutex_IsLocked + + +#ifdef __cplusplus +} +#endif +#endif // !Py_LIMITED_API +#endif // !Py_LOCK_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystate.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystate.h index c1b97d428..f367146e2 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystate.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystate.h @@ -105,13 +105,17 @@ struct _ts { # define _PyThreadState_WHENCE_INIT 1 # define _PyThreadState_WHENCE_FINI 2 # define _PyThreadState_WHENCE_THREADING 3 -# define _PyThreadState_WHENCE_GILSTATE 4 +# define _PyThreadState_WHENCE_C_API 4 # define _PyThreadState_WHENCE_EXEC 5 +# define _PyThreadState_WHENCE_THREADING_DAEMON 6 #endif /* Currently holds the GIL. Must be its own field to avoid data races */ int holds_gil; + /* Currently requesting the GIL */ + int gil_requested; + int _whence; /* Thread state (_Py_THREAD_ATTACHED, _Py_THREAD_DETACHED, _Py_THREAD_SUSPENDED). @@ -131,6 +135,16 @@ struct _ts { /* Pointer to currently executing frame. */ struct _PyInterpreterFrame *current_frame; + /* Pointer to the base frame (bottommost sentinel frame). + Used by profilers to validate complete stack unwinding. + Points to the embedded base_frame in _PyThreadStateImpl. + The frame is embedded there rather than here because _PyInterpreterFrame + is defined in internal headers that cannot be exposed in the public API. */ + struct _PyInterpreterFrame *base_frame; + + struct _PyInterpreterFrame *last_profiled_frame; + uintptr_t last_profiled_frame_seq; + Py_tracefunc c_profilefunc; Py_tracefunc c_tracefunc; PyObject *c_profileobj; @@ -157,6 +171,11 @@ struct _ts { */ unsigned long native_thread_id; + /* List of objects that still need to be cleaned up, singly linked + * via their gc headers' gc_next pointers. The list is populated by + * _PyTrash_thread_deposit_object and cleaned up by + * _PyTrash_thread_destroy_chain. + */ PyObject *delete_later; /* Tagged pointer to top-most critical section, or zero if there is no @@ -180,6 +199,7 @@ struct _ts { _PyStackChunk *datastack_chunk; PyObject **datastack_top; PyObject **datastack_limit; + _PyStackChunk *datastack_cached_chunk; /* XXX signal handlers should also be here */ /* The following fields are here to avoid allocation during init. @@ -198,6 +218,9 @@ struct _ts { PyObject *current_executor; + /* Internal to the JIT */ + struct _PyExitData *jit_exit; + uint64_t dict_global_version; /* Used to store/retrieve `threading.local` keys/values for this thread */ @@ -209,7 +232,28 @@ struct _ts { PyObject *threading_local_sentinel; _PyRemoteDebuggerSupport remote_debugger_support; - _PyStackChunk *datastack_cached_chunk; +#ifdef Py_STATS + // Pointer to PyStats structure, NULL if recording is off. For the + // free-threaded build, the structure is per-thread (stored as a pointer + // in _PyThreadStateImpl). For the default build, the structure is stored + // in the PyInterpreterState structure (threads do not have their own + // structure and all share the same per-interpreter structure). + PyStats *pystats; +#endif + + struct { + /* Number of nested PyThreadState_Ensure() calls on this thread state */ + Py_ssize_t counter; + + /* Should this thread state be deleted upon calling + PyThreadState_Release() (with the counter at 1)? + + This is only true for thread states created by PyThreadState_Ensure() */ + int delete_on_release; + + /* The interpreter guard owned by PyThreadState_EnsureFromView(), if any. */ + PyInterpreterGuard *owned_guard; + } ensure; }; /* other API */ @@ -232,6 +276,21 @@ PyAPI_FUNC(void) PyThreadState_EnterTracing(PyThreadState *tstate); // function is set, otherwise disable them. PyAPI_FUNC(void) PyThreadState_LeaveTracing(PyThreadState *tstate); +#ifdef Py_STATS +#if defined(HAVE_THREAD_LOCAL) && !defined(Py_BUILD_CORE_MODULE) +extern _Py_thread_local PyThreadState *_Py_tss_tstate; + +static inline PyStats* +_PyThreadState_GetStatsFast(void) +{ + if (_Py_tss_tstate == NULL) { + return NULL; // no attached thread state + } + return _Py_tss_tstate->pystats; +} +#endif +#endif // Py_STATS + /* PyGILState */ /* Helper/diagnostic function - return 1 if the current thread @@ -275,3 +334,8 @@ PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc( PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameFunc( PyInterpreterState *interp, _PyFrameEvalFunction eval_frame); +PyAPI_FUNC(void) _PyInterpreterState_SetEvalFrameAllowSpecialization( + PyInterpreterState *interp, + int allow_specialization); +PyAPI_FUNC(int) _PyInterpreterState_IsSpecializationEnabled( + PyInterpreterState *interp); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystats.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystats.h index cf830b606..5d1f44988 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystats.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/pystats.h @@ -4,7 +4,7 @@ // // - _Py_INCREF_STAT_INC() and _Py_DECREF_STAT_INC() used by Py_INCREF() // and Py_DECREF(). -// - _Py_stats variable +// - _PyStats_GET() // // Functions of the sys module: // @@ -14,7 +14,7 @@ // - sys._stats_dump() // // Python must be built with ./configure --enable-pystats to define the -// Py_STATS macro. +// _PyStats_GET() macro. // // Define _PY_INTERPRETER macro to increment interpreter_increfs and // interpreter_decrefs. Otherwise, increment increfs and decrefs. @@ -29,7 +29,7 @@ # error "this header file must not be included directly" #endif -#define PYSTATS_MAX_UOP_ID 1024 +#define PYSTATS_MAX_UOP_ID 2000 #define SPECIALIZATION_FAILURE_KINDS 60 @@ -109,6 +109,18 @@ typedef struct _gc_stats { uint64_t objects_not_transitively_reachable; } GCStats; +#ifdef Py_GIL_DISABLED +// stats specific to free-threaded build +typedef struct _ft_stats { + // number of times interpreter had to spin or park when trying to acquire a mutex + uint64_t mutex_sleeps; + // number of times that the QSBR mechanism polled (compute read sequence value) + uint64_t qsbr_polls; + // number of times stop-the-world mechanism was used + uint64_t world_stops; +} FTStats; +#endif + typedef struct _uop_stats { uint64_t execution_count; uint64_t miss; @@ -130,7 +142,9 @@ typedef struct _optimization_stats { uint64_t recursive_call; uint64_t low_confidence; uint64_t unknown_callee; + uint64_t trace_immediately_deopts; uint64_t executors_invalidated; + uint64_t fitness_terminated_traces; UOpStats opcode[PYSTATS_MAX_UOP_ID + 1]; uint64_t unsupported_opcode[256]; uint64_t trace_length_hist[_Py_UOP_HIST_SIZE]; @@ -138,6 +152,8 @@ typedef struct _optimization_stats { uint64_t optimized_trace_length_hist[_Py_UOP_HIST_SIZE]; uint64_t optimizer_attempts; uint64_t optimizer_successes; + uint64_t optimizer_contradiction; + uint64_t optimizer_frame_overflow; uint64_t optimizer_failure_reason_no_memory; uint64_t remove_globals_builtins_changed; uint64_t remove_globals_incorrect_keys; @@ -173,22 +189,48 @@ typedef struct _stats { CallStats call_stats; ObjectStats object_stats; OptimizationStats optimization_stats; +#ifdef Py_GIL_DISABLED + FTStats ft_stats; +#endif RareEventStats rare_event_stats; - GCStats *gc_stats; + GCStats gc_stats[3]; // must match NUM_GENERATIONS } PyStats; +// Export for most shared extensions +PyAPI_FUNC(PyStats *) _PyStats_GetLocal(void); + +#if defined(HAVE_THREAD_LOCAL) && !defined(Py_BUILD_CORE_MODULE) +// use inline function version defined in cpython/pystate.h +static inline PyStats *_PyThreadState_GetStatsFast(void); +#define _PyStats_GET _PyThreadState_GetStatsFast +#else +#define _PyStats_GET _PyStats_GetLocal +#endif -// Export for shared extensions like 'math' -PyAPI_DATA(PyStats*) _Py_stats; +#define _Py_STATS_EXPR(expr) \ + do { \ + PyStats *s = _PyStats_GET(); \ + if (s != NULL) { \ + s->expr; \ + } \ + } while (0) + +#define _Py_STATS_COND_EXPR(cond, expr) \ + do { \ + PyStats *s = _PyStats_GET(); \ + if (s != NULL && (cond)) { \ + s->expr; \ + } \ + } while (0) #ifdef _PY_INTERPRETER -# define _Py_INCREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.interpreter_increfs++; } while (0) -# define _Py_DECREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.interpreter_decrefs++; } while (0) -# define _Py_INCREF_IMMORTAL_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.interpreter_immortal_increfs++; } while (0) -# define _Py_DECREF_IMMORTAL_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.interpreter_immortal_decrefs++; } while (0) +# define _Py_INCREF_STAT_INC() _Py_STATS_EXPR(object_stats.interpreter_increfs++) +# define _Py_DECREF_STAT_INC() _Py_STATS_EXPR(object_stats.interpreter_decrefs++) +# define _Py_INCREF_IMMORTAL_STAT_INC() _Py_STATS_EXPR(object_stats.interpreter_immortal_increfs++) +# define _Py_DECREF_IMMORTAL_STAT_INC() _Py_STATS_EXPR(object_stats.interpreter_immortal_decrefs++) #else -# define _Py_INCREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.increfs++; } while (0) -# define _Py_DECREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.decrefs++; } while (0) -# define _Py_INCREF_IMMORTAL_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.immortal_increfs++; } while (0) -# define _Py_DECREF_IMMORTAL_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.immortal_decrefs++; } while (0) +# define _Py_INCREF_STAT_INC() _Py_STATS_EXPR(object_stats.increfs++) +# define _Py_DECREF_STAT_INC() _Py_STATS_EXPR(object_stats.decrefs++) +# define _Py_INCREF_IMMORTAL_STAT_INC() _Py_STATS_EXPR(object_stats.immortal_increfs++) +# define _Py_DECREF_IMMORTAL_STAT_INC() _Py_STATS_EXPR(object_stats.immortal_decrefs++) #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/sentinelobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/sentinelobject.h new file mode 100644 index 000000000..e621d6abb --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/sentinelobject.h @@ -0,0 +1,26 @@ +/* Sentinel object interface */ + +#ifndef Py_LIMITED_API +#ifndef _Py_SENTINELOBJECT_H +#define _Py_SENTINELOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +PyAPI_DATA(PyTypeObject) PySentinel_Type; + +#define PySentinel_CheckExact(op) Py_IS_TYPE((op), &PySentinel_Type) + +/* Alias as long as subclasses are not allowed. */ +#define PySentinel_Check(op) PySentinel_CheckExact(op) + +PyAPI_FUNC(PyObject *) PySentinel_New( + const char *name, + const char *module_name, + const char *repr); + +#ifdef __cplusplus +} +#endif +#endif /* !_Py_SENTINELOBJECT_H */ +#endif /* !Py_LIMITED_API */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/sliceobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/sliceobject.h new file mode 100644 index 000000000..137206eff --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/sliceobject.h @@ -0,0 +1,20 @@ +#ifndef _Py_CPYTHON_SLICEOBJECT_H +# error "this header file must not be included directly" +#endif + +/* Slice object interface */ + +/* +A slice object containing start, stop, and step data members (the +names are from range). After much talk with Guido, it was decided to +let these be any arbitrary python type. Py_None stands for omitted values. +*/ +typedef struct { + PyObject_HEAD + PyObject *start, *stop, *step; /* not NULL */ +} PySliceObject; + +PyAPI_FUNC(PyObject *) _PySlice_FromIndices(Py_ssize_t start, Py_ssize_t stop); +PyAPI_FUNC(int) _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, + PyObject **start_ptr, PyObject **stop_ptr, + PyObject **step_ptr); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/structseq.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/structseq.h new file mode 100644 index 000000000..83a1abcd6 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/structseq.h @@ -0,0 +1,12 @@ +#ifndef _Py_CPYTHON_STRUCTSEQ_H +# error "this header file must not be included directly" +#endif + +PyAPI_FUNC(void) PyStructSequence_InitType(PyTypeObject *type, + PyStructSequence_Desc *desc); +PyAPI_FUNC(int) PyStructSequence_InitType2(PyTypeObject *type, + PyStructSequence_Desc *desc); + +typedef PyTupleObject PyStructSequence; +#define PyStructSequence_SET_ITEM PyStructSequence_SetItem +#define PyStructSequence_GET_ITEM PyStructSequence_GetItem diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/traceback.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/traceback.h index 81c51944f..7f42730f1 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/traceback.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/traceback.h @@ -11,3 +11,11 @@ struct _traceback { int tb_lasti; int tb_lineno; }; + +PyAPI_FUNC(const char*) PyUnstable_DumpTraceback(int fd, PyThreadState *tstate); + +PyAPI_FUNC(const char*) PyUnstable_DumpTracebackThreads( + int fd, + PyInterpreterState *interp, + PyThreadState *current_tstate, + Py_ssize_t max_threads); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/tupleobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/tupleobject.h index afb98ccbb..888baaf33 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/tupleobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/tupleobject.h @@ -38,3 +38,7 @@ PyTuple_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) { } #define PyTuple_SET_ITEM(op, index, value) \ PyTuple_SET_ITEM(_PyObject_CAST(op), (index), _PyObject_CAST(value)) + +PyAPI_FUNC(PyObject*) PyTuple_FromArray( + PyObject *const *array, + Py_ssize_t size); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/unicodeobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/unicodeobject.h index 3d0414f52..ea91f4158 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/unicodeobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/unicodeobject.h @@ -47,12 +47,69 @@ static inline Py_UCS4 Py_UNICODE_LOW_SURROGATE(Py_UCS4 ch) { /* --- Unicode Type ------------------------------------------------------- */ +struct _PyUnicodeObject_state { + /* If interned is non-zero, the two references from the + dictionary to this object are *not* counted in ob_refcnt. + The possible values here are: + 0: Not Interned + 1: Interned + 2: Interned and Immortal + 3: Interned, Immortal, and Static + This categorization allows the runtime to determine the right + cleanup mechanism at runtime shutdown. */ +#ifdef Py_GIL_DISABLED + // Needs to be accessed atomically, so can't be a bit field. + unsigned char interned; +#else + unsigned int interned:2; +#endif + /* Character size: + + - PyUnicode_1BYTE_KIND (1): + + * character type = Py_UCS1 (8 bits, unsigned) + * all characters are in the range U+0000-U+00FF (latin1) + * if ascii is set, all characters are in the range U+0000-U+007F + (ASCII), otherwise at least one character is in the range + U+0080-U+00FF + + - PyUnicode_2BYTE_KIND (2): + + * character type = Py_UCS2 (16 bits, unsigned) + * all characters are in the range U+0000-U+FFFF (BMP) + * at least one character is in the range U+0100-U+FFFF + + - PyUnicode_4BYTE_KIND (4): + + * character type = Py_UCS4 (32 bits, unsigned) + * all characters are in the range U+0000-U+10FFFF + * at least one character is in the range U+10000-U+10FFFF + */ + unsigned int kind:3; + /* Compact is with respect to the allocation scheme. Compact unicode + objects only require one memory block while non-compact objects use + one block for the PyUnicodeObject struct and another for its data + buffer. */ + unsigned int compact:1; + /* The string only contains characters in the range U+0000-U+007F (ASCII) + and the kind is PyUnicode_1BYTE_KIND. If ascii is set and compact is + set, use the PyASCIIObject structure. */ + unsigned int ascii:1; + /* The object is statically allocated. */ + unsigned int statically_allocated:1; +#ifndef Py_GIL_DISABLED + /* Historical: padding to ensure that PyUnicode_DATA() is always aligned to + 4 bytes (see issue gh-63736 on m68k) */ + unsigned int :24; +#endif +}; + /* ASCII-only strings created through PyUnicode_New use the PyASCIIObject structure. state.ascii and state.compact are set, and the data immediately follow the structure. utf8_length can be found in the length field; the utf8 pointer is equal to the data pointer. */ typedef struct { - /* There are 4 forms of Unicode strings: + /* There are 3 forms of Unicode strings: - compact ascii: @@ -99,67 +156,8 @@ typedef struct { PyObject_HEAD Py_ssize_t length; /* Number of code points in the string */ Py_hash_t hash; /* Hash value; -1 if not set */ -#ifdef Py_GIL_DISABLED - /* Ensure 4 byte alignment for PyUnicode_DATA(), see gh-63736 on m68k. - In the non-free-threaded build, we'll use explicit padding instead */ - _Py_ALIGN_AS(4) -#endif - struct { - /* If interned is non-zero, the two references from the - dictionary to this object are *not* counted in ob_refcnt. - The possible values here are: - 0: Not Interned - 1: Interned - 2: Interned and Immortal - 3: Interned, Immortal, and Static - This categorization allows the runtime to determine the right - cleanup mechanism at runtime shutdown. */ -#ifdef Py_GIL_DISABLED - // Needs to be accessed atomically, so can't be a bit field. - unsigned char interned; -#else - unsigned int interned:2; -#endif - /* Character size: - - - PyUnicode_1BYTE_KIND (1): - - * character type = Py_UCS1 (8 bits, unsigned) - * all characters are in the range U+0000-U+00FF (latin1) - * if ascii is set, all characters are in the range U+0000-U+007F - (ASCII), otherwise at least one character is in the range - U+0080-U+00FF - - - PyUnicode_2BYTE_KIND (2): - - * character type = Py_UCS2 (16 bits, unsigned) - * all characters are in the range U+0000-U+FFFF (BMP) - * at least one character is in the range U+0100-U+FFFF - - - PyUnicode_4BYTE_KIND (4): - - * character type = Py_UCS4 (32 bits, unsigned) - * all characters are in the range U+0000-U+10FFFF - * at least one character is in the range U+10000-U+10FFFF - */ - unsigned int kind:3; - /* Compact is with respect to the allocation scheme. Compact unicode - objects only require one memory block while non-compact objects use - one block for the PyUnicodeObject struct and another for its data - buffer. */ - unsigned int compact:1; - /* The string only contains characters in the range U+0000-U+007F (ASCII) - and the kind is PyUnicode_1BYTE_KIND. If ascii is set and compact is - set, use the PyASCIIObject structure. */ - unsigned int ascii:1; - /* The object is statically allocated. */ - unsigned int statically_allocated:1; -#ifndef Py_GIL_DISABLED - /* Padding to ensure that PyUnicode_DATA() is always aligned to - 4 bytes (see issue gh-63736 on m68k) */ - unsigned int :24; -#endif - } state; + /* Ensure 4 byte alignment for PyUnicode_DATA(), see gh-63736 on m68k. */ + _Py_ALIGNED_DEF(4, struct _PyUnicodeObject_state) state; } PyASCIIObject; /* Non-ASCII strings allocated through PyUnicode_New use the @@ -300,6 +298,16 @@ static inline Py_ssize_t PyUnicode_GET_LENGTH(PyObject *op) { } #define PyUnicode_GET_LENGTH(op) PyUnicode_GET_LENGTH(_PyObject_CAST(op)) +/* Returns the cached hash, or -1 if not cached yet. */ +static inline Py_hash_t +PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) { +#ifdef Py_GIL_DISABLED + return _Py_atomic_load_ssize_relaxed(&_PyASCIIObject_CAST(op)->hash); +#else + return _PyASCIIObject_CAST(op)->hash; +#endif +} + /* Write into the canonical representation, this function does not do any sanity checks and is intended for usage in loops. The caller should cache the kind and data pointers obtained from other function calls. @@ -488,7 +496,7 @@ PyAPI_FUNC(int) PyUnicodeWriter_WriteWideChar( Py_ssize_t size); PyAPI_FUNC(int) PyUnicodeWriter_WriteUCS4( PyUnicodeWriter *writer, - Py_UCS4 *str, + const Py_UCS4 *str, Py_ssize_t size); PyAPI_FUNC(int) PyUnicodeWriter_WriteStr( @@ -770,4 +778,4 @@ static inline int Py_UNICODE_ISALNUM(Py_UCS4 ch) { // Return an interned Unicode object for an Identifier; may fail if there is no // memory. -PyAPI_FUNC(PyObject*) _PyUnicode_FromId(_Py_Identifier*); +Py_DEPRECATED(3.15) PyAPI_FUNC(PyObject*) _PyUnicode_FromId(_Py_Identifier*); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/weakrefobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/weakrefobject.h index da8e77cdd..e0711407c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/cpython/weakrefobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/cpython/weakrefobject.h @@ -47,20 +47,3 @@ PyAPI_FUNC(void) _PyWeakref_ClearRef(PyWeakReference *self); // Test if a weak reference is dead. PyAPI_FUNC(int) PyWeakref_IsDead(PyObject *ref); - -Py_DEPRECATED(3.13) static inline PyObject* PyWeakref_GET_OBJECT(PyObject *ref_obj) -{ - PyWeakReference *ref = _PyWeakref_CAST(ref_obj); - PyObject *obj = ref->wr_object; - // Explanation for the Py_REFCNT() check: when a weakref's target is part - // of a long chain of deallocations which triggers the trashcan mechanism, - // clearing the weakrefs can be delayed long after the target's refcount - // has dropped to zero. In the meantime, code accessing the weakref will - // be able to "see" the target object even though it is supposed to be - // unreachable. See issue gh-60806. - if (Py_REFCNT(obj) > 0) { - return obj; - } - return Py_None; -} -#define PyWeakref_GET_OBJECT(ref) PyWeakref_GET_OBJECT(_PyObject_CAST(ref)) diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/critical_section.h b/python-multiplatform/src/nativeInterop/cinterop/include/critical_section.h index 3b37615a8..732bfab7e 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/critical_section.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/critical_section.h @@ -4,6 +4,91 @@ extern "C" { #endif +// Python critical sections +// +// Conceptually, critical sections are a deadlock avoidance layer on top of +// per-object locks. These helpers, in combination with those locks, replace +// our usage of the global interpreter lock to provide thread-safety for +// otherwise thread-unsafe objects, such as dict. +// +// NOTE: These APIs are no-ops in non-free-threaded builds. +// +// NOTE: Only the top-most critical section is guaranteed to be active. +// Operations that need to lock two objects at once must use +// `Py_BEGIN_CRITICAL_SECTION2()`. You *CANNOT* use nested critical sections +// to lock more than one object at once, because the inner critical section +// may suspend the outer critical sections. This API does not provide a way +// to lock more than two objects at once (though it could be added later +// if actually needed). +// +// NOTE: Critical sections implicitly behave like reentrant locks because +// attempting to acquire the same lock will suspend any outer (earlier) +// critical sections. However, they are less efficient for this use case than +// purposefully designed reentrant locks. +// +// Example usage: +// Py_BEGIN_CRITICAL_SECTION(op); +// ... +// Py_END_CRITICAL_SECTION(); +// +// To lock two objects at once: +// Py_BEGIN_CRITICAL_SECTION2(op1, op2); +// ... +// Py_END_CRITICAL_SECTION2(); + +// NOTE: the contents of this struct are private and their meaning may +// change betweeen Python releases without a deprecation period. +typedef struct PyCriticalSection { + // Tagged pointer to an outer active critical section (or 0). + uintptr_t _cs_prev; + + // Mutex used to protect critical section + struct PyMutex *_cs_mutex; +} PyCriticalSection; + +// A critical section protected by two mutexes. Use +// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2. +// NOTE: the contents of this struct are private and may change betweeen +// Python releases without a deprecation period. +typedef struct PyCriticalSection2 { + PyCriticalSection _cs_base; + + struct PyMutex *_cs_mutex2; +} PyCriticalSection2; + +PyAPI_FUNC(void) +PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op); + +PyAPI_FUNC(void) +PyCriticalSection_End(PyCriticalSection *c); + +PyAPI_FUNC(void) +PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b); + +PyAPI_FUNC(void) +PyCriticalSection2_End(PyCriticalSection2 *c); + +// These are definitions for the stable ABI. For GIL-ful builds they're +// conditionally redefined as no-ops in cpython/critical_section.h. + +# define Py_BEGIN_CRITICAL_SECTION(op) \ + { \ + PyCriticalSection _py_cs; \ + PyCriticalSection_Begin(&_py_cs, _PyObject_CAST(op)) + +# define Py_END_CRITICAL_SECTION() \ + PyCriticalSection_End(&_py_cs); \ + } + +# define Py_BEGIN_CRITICAL_SECTION2(a, b) \ + { \ + PyCriticalSection2 _py_cs2; \ + PyCriticalSection2_Begin(&_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b)) + +# define Py_END_CRITICAL_SECTION2() \ + PyCriticalSection2_End(&_py_cs2); \ + } + #ifndef Py_LIMITED_API # define Py_CPYTHON_CRITICAL_SECTION_H # include "cpython/critical_section.h" diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/datetime.h b/python-multiplatform/src/nativeInterop/cinterop/include/datetime.h index b78cc0e8e..66e6c6e3a 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/datetime.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/datetime.h @@ -1,8 +1,8 @@ /* datetime.h */ #ifndef Py_LIMITED_API -#ifndef DATETIME_H -#define DATETIME_H +#ifndef Py_DATETIME_H +#define Py_DATETIME_H #ifdef __cplusplus extern "C" { #endif @@ -196,8 +196,23 @@ typedef struct { /* Define global variable for the C API and a macro for setting it. */ static PyDateTime_CAPI *PyDateTimeAPI = NULL; -#define PyDateTime_IMPORT \ - PyDateTimeAPI = (PyDateTime_CAPI *)PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0) +static inline PyDateTime_CAPI * +_PyDateTime_IMPORT(void) { + PyDateTime_CAPI *val = (PyDateTime_CAPI *)_Py_atomic_load_ptr(&PyDateTimeAPI); + if (val == NULL) { + PyDateTime_CAPI *capi = (PyDateTime_CAPI *)PyCapsule_Import( + PyDateTime_CAPSULE_NAME, 0); + if (capi != NULL) { + /* if the compare exchange fails then in that case + another thread would have initialized it */ + _Py_atomic_compare_exchange_ptr(&PyDateTimeAPI, &val, (void *)capi); + return capi; + } + } + return val; +} + +#define PyDateTime_IMPORT _PyDateTime_IMPORT() /* Macro for access to the UTC singleton */ #define PyDateTime_TimeZone_UTC PyDateTimeAPI->TimeZone_UTC @@ -263,5 +278,5 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL; #ifdef __cplusplus } #endif -#endif +#endif /* !Py_DATETIME_H */ #endif /* !Py_LIMITED_API */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/descrobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/descrobject.h index fd66d17b4..340de4e0e 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/descrobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/descrobject.h @@ -80,10 +80,14 @@ struct PyMemberDef { #define _Py_T_NONE 20 // Deprecated. Value is always None. /* Flags */ -#define Py_READONLY 1 -#define Py_AUDIT_READ 2 // Added in 3.10, harmless no-op before that -#define _Py_WRITE_RESTRICTED 4 // Deprecated, no-op. Do not reuse the value. -#define Py_RELATIVE_OFFSET 8 +#define Py_READONLY (1 << 0) +#define Py_AUDIT_READ (1 << 1) // Added in 3.10, harmless no-op before that +#define _Py_WRITE_RESTRICTED (1 << 2) // Deprecated, no-op. Do not reuse the value. +#define Py_RELATIVE_OFFSET (1 << 3) + +#ifndef Py_LIMITED_API +# define _Py_AFTER_ITEMS (1 << 4) // For internal use. +#endif PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *); PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/dictobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/dictobject.h index 1bbeec1ab..0384e3131 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/dictobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/dictobject.h @@ -68,6 +68,18 @@ PyAPI_FUNC(int) PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result PyAPI_FUNC(int) PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result); #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030F0000 +// Inserts `key` with a value `default_value`, if `key` is not already present +// in the dictionary. If `result` is not NULL, then the value associated +// with `key` is returned in `*result` (either the existing value, or the now +// inserted `default_value`). +// Returns: +// -1 on error +// 0 if `key` was not present and `default_value` was inserted +// 1 if `key` was present and `default_value` was not inserted +PyAPI_FUNC(int) PyDict_SetDefaultRef(PyObject *mp, PyObject *key, PyObject *default_value, PyObject **result); +#endif + #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000 PyAPI_FUNC(PyObject *) PyObject_GenericGetDict(PyObject *, void *); #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/exports.h b/python-multiplatform/src/nativeInterop/cinterop/include/exports.h index 0c646d5be..186922830 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/exports.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/exports.h @@ -9,6 +9,7 @@ inside the Python core, they are private to the core. If in an extension module, it may be declared with external linkage depending on the platform. + PyMODEXPORT_FUNC: Like PyMODINIT_FUNC, but for a slots array As a number of platforms support/require "__declspec(dllimport/dllexport)", we support a HAVE_DECLSPEC_DLL macro to save duplication. @@ -34,6 +35,12 @@ #define Py_EXPORTED_SYMBOL #define Py_LOCAL_SYMBOL #endif + /* module init functions outside the core must be exported */ + #if defined(_PyEXPORTS_CORE) + #define _PyINIT_EXPORTED_SYMBOL Py_EXPORTED_SYMBOL + #else + #define _PyINIT_EXPORTED_SYMBOL __declspec(dllexport) + #endif #else /* * If we only ever used gcc >= 5, we could use __has_attribute(visibility) @@ -51,22 +58,19 @@ #define Py_EXPORTED_SYMBOL #define Py_LOCAL_SYMBOL #endif + #define _PyINIT_EXPORTED_SYMBOL Py_EXPORTED_SYMBOL #endif /* only get special linkage if built as shared or platform is Cygwin */ #if defined(Py_ENABLE_SHARED) || defined(__CYGWIN__) # if defined(HAVE_DECLSPEC_DLL) -# if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) -# define PyAPI_FUNC(RTYPE) Py_EXPORTED_SYMBOL RTYPE -# define PyAPI_DATA(RTYPE) extern Py_EXPORTED_SYMBOL RTYPE +# if defined(_PyEXPORTS_CORE) && !defined(_PyEXPORTS_CORE_MODULE) /* module init functions inside the core need no external linkage */ /* except for Cygwin to handle embedding */ -# if defined(__CYGWIN__) -# define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject* -# else /* __CYGWIN__ */ -# define PyMODINIT_FUNC PyObject* +# if !defined(__CYGWIN__) +# define _PyINIT_FUNC_DECLSPEC # endif /* __CYGWIN__ */ -# else /* Py_BUILD_CORE */ +# else /* _PyEXPORTS_CORE */ /* Building an extension module, or an embedded situation */ /* public Python functions and data are imported */ /* Under Cygwin, auto-import functions to prevent compilation */ @@ -76,13 +80,7 @@ # define PyAPI_FUNC(RTYPE) Py_IMPORTED_SYMBOL RTYPE # endif /* !__CYGWIN__ */ # define PyAPI_DATA(RTYPE) extern Py_IMPORTED_SYMBOL RTYPE - /* module init functions outside the core must be exported */ -# if defined(__cplusplus) -# define PyMODINIT_FUNC extern "C" Py_EXPORTED_SYMBOL PyObject* -# else /* __cplusplus */ -# define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject* -# endif /* __cplusplus */ -# endif /* Py_BUILD_CORE */ +# endif /* _PyEXPORTS_CORE */ # endif /* HAVE_DECLSPEC_DLL */ #endif /* Py_ENABLE_SHARED */ @@ -93,13 +91,19 @@ #ifndef PyAPI_DATA # define PyAPI_DATA(RTYPE) extern Py_EXPORTED_SYMBOL RTYPE #endif -#ifndef PyMODINIT_FUNC +#ifndef _PyINIT_FUNC_DECLSPEC # if defined(__cplusplus) -# define PyMODINIT_FUNC extern "C" Py_EXPORTED_SYMBOL PyObject* +# define _PyINIT_FUNC_DECLSPEC extern "C" _PyINIT_EXPORTED_SYMBOL # else /* __cplusplus */ -# define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject* +# define _PyINIT_FUNC_DECLSPEC _PyINIT_EXPORTED_SYMBOL # endif /* __cplusplus */ #endif +#ifndef PyMODINIT_FUNC + #define PyMODINIT_FUNC _PyINIT_FUNC_DECLSPEC PyObject* +#endif +#ifndef PyMODEXPORT_FUNC + #define PyMODEXPORT_FUNC _PyINIT_FUNC_DECLSPEC PySlot* +#endif #endif /* Py_EXPORTS_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/fileutils.h b/python-multiplatform/src/nativeInterop/cinterop/include/fileutils.h index 1509198e4..00c37fa46 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/fileutils.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/fileutils.h @@ -16,7 +16,7 @@ # define S_IFMT 0170000 #endif #ifndef S_IFLNK - // Windows doesn't define S_IFLNK, but posixmodule.c maps + // Windows doesn't define S_IFLNK, but fileutils.c maps // IO_REPARSE_TAG_SYMLINK to S_IFLNK. # define S_IFLNK 0120000 #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/floatobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/floatobject.h index 4d24a76ed..814337b07 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/floatobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/floatobject.h @@ -18,14 +18,14 @@ PyAPI_DATA(PyTypeObject) PyFloat_Type; #define Py_RETURN_NAN return PyFloat_FromDouble(Py_NAN) -#define Py_RETURN_INF(sign) \ - do { \ - if (copysign(1., sign) == 1.) { \ - return PyFloat_FromDouble(Py_INFINITY); \ - } \ - else { \ - return PyFloat_FromDouble(-Py_INFINITY); \ - } \ +#define Py_RETURN_INF(sign) \ + do { \ + if (copysign(1., sign) == 1.) { \ + return PyFloat_FromDouble(INFINITY); \ + } \ + else { \ + return PyFloat_FromDouble(-INFINITY); \ + } \ } while(0) PyAPI_FUNC(double) PyFloat_GetMax(void); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/import.h b/python-multiplatform/src/nativeInterop/cinterop/include/import.h index 24b23b911..c062e46bf 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/import.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/import.h @@ -51,9 +51,6 @@ PyAPI_FUNC(PyObject *) PyImport_AddModuleRef( PyAPI_FUNC(PyObject *) PyImport_ImportModule( const char *name /* UTF-8 encoded string */ ); -Py_DEPRECATED(3.13) PyAPI_FUNC(PyObject *) PyImport_ImportModuleNoBlock( - const char *name /* UTF-8 encoded string */ - ); PyAPI_FUNC(PyObject *) PyImport_ImportModuleLevel( const char *name, /* UTF-8 encoded string */ PyObject *globals, @@ -91,6 +88,19 @@ PyAPI_FUNC(int) PyImport_AppendInittab( PyObject* (*initfunc)(void) ); +typedef enum { + PyImport_LAZY_NORMAL, + PyImport_LAZY_ALL +} PyImport_LazyImportsMode; + +#ifndef Py_LIMITED_API +PyAPI_FUNC(int) PyImport_SetLazyImportsMode(PyImport_LazyImportsMode mode); +PyAPI_FUNC(int) PyImport_SetLazyImportsFilter(PyObject *filter); + +PyAPI_FUNC(PyImport_LazyImportsMode) PyImport_GetLazyImportsMode(void); +PyAPI_FUNC(PyObject *) PyImport_GetLazyImportsFilter(void); +#endif + #ifndef Py_LIMITED_API # define Py_CPYTHON_IMPORT_H # include "cpython/import.h" diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/mimalloc/mimalloc/types.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/mimalloc/mimalloc/types.h index 70c600e92..286e7bf66 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/mimalloc/mimalloc/types.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/mimalloc/mimalloc/types.h @@ -50,6 +50,32 @@ terms of the MIT license. A copy of the license can be found in the file #define mi_decl_cache_align #endif +#if (MI_DEBUG) +#if defined(_MSC_VER) +#define mi_decl_noreturn __declspec(noreturn) +#elif (defined(__GNUC__) && (__GNUC__ >= 3)) || defined(__clang__) +#define mi_decl_noreturn __attribute__((__noreturn__)) +#else +#define mi_decl_noreturn +#endif + +/* + * 'cold' attribute seems to have been fully supported since GCC 4.x. + * See https://github.com/gcc-mirror/gcc/commit/52bf96d2f299e9e6. + */ +#if (defined(__GNUC__) && (__GNUC__ >= 4)) || defined(__clang__) +#define mi_decl_cold __attribute__((cold)) +#else +#define mi_decl_cold +#endif + +#if (defined(__GNUC__) && defined(__THROW)) +#define mi_decl_throw __THROW +#else +#define mi_decl_throw +#endif +#endif + // ------------------------------------------------------ // Variants // ------------------------------------------------------ @@ -455,7 +481,7 @@ typedef struct mi_segment_s { struct mi_segment_s* next; // the list of freed segments in the cache (must be first field, see `segment.c:mi_segment_init`) size_t abandoned; // abandoned pages (i.e. the original owning thread stopped) (`abandoned <= used`) - size_t abandoned_visits; // count how often this segment is visited in the abandoned list (to force reclaim it it is too long) + size_t abandoned_visits; // count how often this segment is visited in the abandoned list (to force reclaim if it is too long) size_t used; // count of pages in use uintptr_t cookie; // verify addresses in debug mode: `mi_ptr_cookie(segment) == segment->cookie` @@ -582,7 +608,8 @@ struct mi_heap_s { #if (MI_DEBUG) // use our own assertion to print without memory allocation -void _mi_assert_fail(const char* assertion, const char* fname, unsigned int line, const char* func ); +mi_decl_noreturn mi_decl_cold +void _mi_assert_fail(const char* assertion, const char* fname, unsigned int line, const char* func) mi_decl_throw; #define mi_assert(expr) ((expr) ? (void)0 : _mi_assert_fail(#expr,__FILE__,__LINE__,__func__)) #else #define mi_assert(x) diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_abstract.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_abstract.h index 3cc0afac4..67c6fa7c0 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_abstract.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_abstract.h @@ -16,10 +16,11 @@ _PyIndex_Check(PyObject *obj) return (tp_as_number != NULL && tp_as_number->nb_index != NULL); } -PyObject *_PyNumber_PowerNoMod(PyObject *lhs, PyObject *rhs); -PyObject *_PyNumber_InPlacePowerNoMod(PyObject *lhs, PyObject *rhs); +// Exported for external JIT support +PyAPI_FUNC(PyObject *) _PyNumber_PowerNoMod(PyObject *lhs, PyObject *rhs); +PyAPI_FUNC(PyObject *) _PyNumber_InPlacePowerNoMod(PyObject *lhs, PyObject *rhs); -extern int _PyObject_HasLen(PyObject *o); +PyAPI_FUNC(int) _PyObject_HasLen(PyObject *o); /* === Sequence protocol ================================================ */ @@ -51,10 +52,22 @@ extern int _PyObject_RealIsSubclass(PyObject *derived, PyObject *cls); // Export for '_bisect' shared extension. PyAPI_FUNC(int) _Py_convert_optional_to_ssize_t(PyObject *, void *); +// Convert Python int to Py_ssize_t. Do nothing if the argument is None. +// Raises ValueError if argument is negative. +PyAPI_FUNC(int) _Py_convert_optional_to_non_negative_ssize_t(PyObject *, void *); + // Same as PyNumber_Index() but can return an instance of a subclass of int. // Export for 'math' shared extension. PyAPI_FUNC(PyObject*) _PyNumber_Index(PyObject *o); +typedef struct { + PyObject *object; + PySendResult kind; +} PySendResultPair; + +// Same as PyIter_Send but returns a struct for MSVC tailcall support +PyAPI_FUNC(PySendResultPair) _PyIter_Send(PyObject *iter, PyObject *arg); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast.h index 60367202b..b47398669 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast.h @@ -329,12 +329,14 @@ struct _stmt { struct { asdl_alias_seq *names; + int is_lazy; } Import; struct { identifier module; asdl_alias_seq *names; int level; + int is_lazy; } ImportFrom; struct { @@ -764,11 +766,12 @@ stmt_ty _PyAST_TryStar(asdl_stmt_seq * body, asdl_excepthandler_seq * handlers, end_col_offset, PyArena *arena); stmt_ty _PyAST_Assert(expr_ty test, expr_ty msg, int lineno, int col_offset, int end_lineno, int end_col_offset, PyArena *arena); -stmt_ty _PyAST_Import(asdl_alias_seq * names, int lineno, int col_offset, int - end_lineno, int end_col_offset, PyArena *arena); +stmt_ty _PyAST_Import(asdl_alias_seq * names, int is_lazy, int lineno, int + col_offset, int end_lineno, int end_col_offset, PyArena + *arena); stmt_ty _PyAST_ImportFrom(identifier module, asdl_alias_seq * names, int level, - int lineno, int col_offset, int end_lineno, int - end_col_offset, PyArena *arena); + int is_lazy, int lineno, int col_offset, int + end_lineno, int end_col_offset, PyArena *arena); stmt_ty _PyAST_Global(asdl_identifier_seq * names, int lineno, int col_offset, int end_lineno, int end_col_offset, PyArena *arena); stmt_ty _PyAST_Nonlocal(asdl_identifier_seq * names, int lineno, int diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast_state.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast_state.h index d4ac419f5..32c12fb58 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast_state.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ast_state.h @@ -161,6 +161,7 @@ struct ast_state { PyObject *__module__; PyObject *_attributes; PyObject *_fields; + PyObject *abstract_types; PyObject *alias_type; PyObject *annotation; PyObject *arg; @@ -205,6 +206,7 @@ struct ast_state { PyObject *id; PyObject *ifs; PyObject *is_async; + PyObject *is_lazy; PyObject *items; PyObject *iter; PyObject *key; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_backoff.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_backoff.h index 942d8b107..9d1912cc2 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_backoff.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_backoff.h @@ -12,6 +12,7 @@ extern "C" { #include #include #include "pycore_structs.h" // _Py_BackoffCounter +#include "pycore_interp_structs.h" // _PyOptimizationConfig /* 16-bit countdown counters using exponential backoff. @@ -22,33 +23,48 @@ extern "C" { Another use is for the Tier 2 optimizer to decide when to create a new Tier 2 trace (executor). Again, exponential backoff is used. - The 16-bit counter is structured as a 12-bit unsigned 'value' - and a 4-bit 'backoff' field. When resetting the counter, the + The 16-bit counter is structured as a 13-bit unsigned 'value' + and a 3-bit 'backoff' field. When resetting the counter, the backoff field is incremented (until it reaches a limit) and the - value is set to a bit mask representing the value 2**backoff - 1. - The maximum backoff is 12 (the number of bits in the value). + value is set to a bit mask representing some prime value - 1. + New values and backoffs for each backoff are calculated once + at compile time and saved to value_and_backoff_next table. + The maximum backoff is 6, since 7 is an UNREACHABLE_BACKOFF. There is an exceptional value which must not be updated, 0xFFFF. */ -#define BACKOFF_BITS 4 -#define MAX_BACKOFF 12 -#define UNREACHABLE_BACKOFF 15 - -static inline bool -is_unreachable_backoff_counter(_Py_BackoffCounter counter) -{ - return counter.value_and_backoff == UNREACHABLE_BACKOFF; -} +#define BACKOFF_BITS 3 +#define BACKOFF_MASK 7 +#define MAX_BACKOFF 6 +#define UNREACHABLE_BACKOFF 7 +#define MAX_VALUE 0x1FFF + +#define MAKE_VALUE_AND_BACKOFF(value, backoff) \ + ((uint16_t)(((value) << BACKOFF_BITS) | (backoff))) + +// For previous backoff b we use value x such that +// x + 1 is near to 2**(2*b+1) and x + 1 is prime. +static const uint16_t value_and_backoff_next[] = { + MAKE_VALUE_AND_BACKOFF(1, 1), + MAKE_VALUE_AND_BACKOFF(6, 2), + MAKE_VALUE_AND_BACKOFF(30, 3), + MAKE_VALUE_AND_BACKOFF(126, 4), + MAKE_VALUE_AND_BACKOFF(508, 5), + MAKE_VALUE_AND_BACKOFF(2052, 6), + // We use the same backoff counter for all backoffs >= MAX_BACKOFF. + MAKE_VALUE_AND_BACKOFF(8190, 6), + MAKE_VALUE_AND_BACKOFF(8190, 6), +}; static inline _Py_BackoffCounter make_backoff_counter(uint16_t value, uint16_t backoff) { - assert(backoff <= 15); - assert(value <= 0xFFF); - _Py_BackoffCounter result; - result.value_and_backoff = (value << BACKOFF_BITS) | backoff; - return result; + assert(backoff <= UNREACHABLE_BACKOFF); + assert(value <= MAX_VALUE); + return ((_Py_BackoffCounter){ + .value_and_backoff = MAKE_VALUE_AND_BACKOFF(value, backoff) + }); } static inline _Py_BackoffCounter @@ -62,14 +78,11 @@ forge_backoff_counter(uint16_t counter) static inline _Py_BackoffCounter restart_backoff_counter(_Py_BackoffCounter counter) { - assert(!is_unreachable_backoff_counter(counter)); - int backoff = counter.value_and_backoff & 15; - if (backoff < MAX_BACKOFF) { - return make_backoff_counter((1 << (backoff + 1)) - 1, backoff + 1); - } - else { - return make_backoff_counter((1 << MAX_BACKOFF) - 1, MAX_BACKOFF); - } + uint16_t backoff = counter.value_and_backoff & BACKOFF_MASK; + assert(backoff <= MAX_BACKOFF); + return ((_Py_BackoffCounter){ + .value_and_backoff = value_and_backoff_next[backoff] + }); } static inline _Py_BackoffCounter @@ -95,29 +108,66 @@ backoff_counter_triggers(_Py_BackoffCounter counter) return counter.value_and_backoff < UNREACHABLE_BACKOFF; } -/* Initial JUMP_BACKWARD counter. - * This determines when we create a trace for a loop. */ -#define JUMP_BACKWARD_INITIAL_VALUE 4095 -#define JUMP_BACKWARD_INITIAL_BACKOFF 12 +static inline bool +backoff_counter_is_unreachable(_Py_BackoffCounter counter) +{ + return (counter.value_and_backoff & BACKOFF_MASK) == UNREACHABLE_BACKOFF; +} + +static inline _Py_BackoffCounter +trigger_backoff_counter(void) +{ + _Py_BackoffCounter result; + result.value_and_backoff = 0; + return result; +} + +// Initial JUMP_BACKWARD counter. +// Must be larger than ADAPTIVE_COOLDOWN_VALUE, otherwise when JIT code is +// invalidated we may construct a new trace before the bytecode has properly +// re-specialized: +// Note: this should be a prime number-1. This increases the likelihood of +// finding a "good" loop iteration to trace. +// For example, 4095 does not work for the nqueens benchmark on pyperformance +// as we always end up tracing the loop iteration's +// exhaustion iteration. Which aborts our current tracer. +#define JUMP_BACKWARD_INITIAL_VALUE 4000 +#define JUMP_BACKWARD_INITIAL_BACKOFF 6 +static inline _Py_BackoffCounter +initial_jump_backoff_counter(_PyOptimizationConfig *opt_config) +{ + return make_backoff_counter( + opt_config->jump_backward_initial_value, + opt_config->jump_backward_initial_backoff); +} + +// This needs to be around 2-4x of JUMP_BACKWARD_INITIAL_VALUE +// The reasoning is that we always want loop traces to form and inline +// functions before functions themselves warm up and link to them instead +// of inlining. +#define RESUME_INITIAL_VALUE 8190 +#define RESUME_INITIAL_BACKOFF 6 static inline _Py_BackoffCounter -initial_jump_backoff_counter(void) +initial_resume_backoff_counter(_PyOptimizationConfig *opt_config) { - return make_backoff_counter(JUMP_BACKWARD_INITIAL_VALUE, - JUMP_BACKWARD_INITIAL_BACKOFF); + return make_backoff_counter( + opt_config->resume_initial_value, + opt_config->resume_initial_backoff); } /* Initial exit temperature. * Must be larger than ADAPTIVE_COOLDOWN_VALUE, * otherwise when a side exit warms up we may construct * a new trace before the Tier 1 code has properly re-specialized. */ -#define SIDE_EXIT_INITIAL_VALUE 4095 -#define SIDE_EXIT_INITIAL_BACKOFF 12 +#define SIDE_EXIT_INITIAL_VALUE 4000 +#define SIDE_EXIT_INITIAL_BACKOFF 6 static inline _Py_BackoffCounter -initial_temperature_backoff_counter(void) +initial_temperature_backoff_counter(_PyOptimizationConfig *opt_config) { - return make_backoff_counter(SIDE_EXIT_INITIAL_VALUE, - SIDE_EXIT_INITIAL_BACKOFF); + return make_backoff_counter( + opt_config->side_exit_initial_value, + opt_config->side_exit_initial_backoff); } /* Unreachable backoff counter. */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_blocks_output_buffer.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_blocks_output_buffer.h index 573e10359..322c1e933 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_blocks_output_buffer.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_blocks_output_buffer.h @@ -45,12 +45,14 @@ extern "C" { #endif typedef struct { - // List of bytes objects - PyObject *list; + // Bytes writer managing output buffer + PyBytesWriter *writer; // Number of whole allocated size Py_ssize_t allocated; - // Max length of the buffer, negative number means unlimited length. + // Max length of the buffer, negative number means unlimited length Py_ssize_t max_length; + // Number of blocks of bytes. Used to calculate next allocation size + size_t num_blocks; } _BlocksOutputBuffer; static const char unable_allocate_msg[] = "Unable to allocate output buffer."; @@ -107,11 +109,10 @@ _BlocksOutputBuffer_InitAndGrow(_BlocksOutputBuffer *buffer, const Py_ssize_t max_length, void **next_out) { - PyObject *b; Py_ssize_t block_size; - // ensure .list was set to NULL - assert(buffer->list == NULL); + // ensure .writer was set to NULL + assert(buffer->writer == NULL); // get block size if (0 <= max_length && max_length < BUFFER_BLOCK_SIZE[0]) { @@ -120,25 +121,17 @@ _BlocksOutputBuffer_InitAndGrow(_BlocksOutputBuffer *buffer, block_size = BUFFER_BLOCK_SIZE[0]; } - // the first block - b = PyBytes_FromStringAndSize(NULL, block_size); - if (b == NULL) { + buffer->writer = PyBytesWriter_Create(block_size); + if (buffer->writer == NULL) { return -1; } - // create the list - buffer->list = PyList_New(1); - if (buffer->list == NULL) { - Py_DECREF(b); - return -1; - } - PyList_SET_ITEM(buffer->list, 0, b); - // set variables buffer->allocated = block_size; buffer->max_length = max_length; + buffer->num_blocks = 1; - *next_out = PyBytes_AS_STRING(b); + *next_out = PyBytesWriter_GetData(buffer->writer); return block_size; } @@ -155,31 +148,21 @@ _BlocksOutputBuffer_InitWithSize(_BlocksOutputBuffer *buffer, const Py_ssize_t init_size, void **next_out) { - PyObject *b; - // ensure .list was set to NULL - assert(buffer->list == NULL); + // ensure .writer was set to NULL + assert(buffer->writer == NULL); - // the first block - b = PyBytes_FromStringAndSize(NULL, init_size); - if (b == NULL) { - PyErr_SetString(PyExc_MemoryError, unable_allocate_msg); + buffer->writer = PyBytesWriter_Create(init_size); + if (buffer->writer == NULL) { return -1; } - // create the list - buffer->list = PyList_New(1); - if (buffer->list == NULL) { - Py_DECREF(b); - return -1; - } - PyList_SET_ITEM(buffer->list, 0, b); - // set variables buffer->allocated = init_size; buffer->max_length = -1; + buffer->num_blocks = 1; - *next_out = PyBytes_AS_STRING(b); + *next_out = PyBytesWriter_GetData(buffer->writer); return init_size; } @@ -193,8 +176,6 @@ _BlocksOutputBuffer_Grow(_BlocksOutputBuffer *buffer, void **next_out, const Py_ssize_t avail_out) { - PyObject *b; - const Py_ssize_t list_len = Py_SIZE(buffer->list); Py_ssize_t block_size; // ensure no gaps in the data @@ -205,11 +186,10 @@ _BlocksOutputBuffer_Grow(_BlocksOutputBuffer *buffer, } // get block size - if (list_len < (Py_ssize_t) Py_ARRAY_LENGTH(BUFFER_BLOCK_SIZE)) { - block_size = BUFFER_BLOCK_SIZE[list_len]; - } else { - block_size = BUFFER_BLOCK_SIZE[Py_ARRAY_LENGTH(BUFFER_BLOCK_SIZE) - 1]; - } + size_t maxblock = Py_ARRAY_LENGTH(BUFFER_BLOCK_SIZE); + assert(maxblock >= 1); + size_t block_index = Py_MIN(buffer->num_blocks, maxblock - 1); + block_size = BUFFER_BLOCK_SIZE[block_index]; // check max_length if (buffer->max_length >= 0) { @@ -229,22 +209,19 @@ _BlocksOutputBuffer_Grow(_BlocksOutputBuffer *buffer, return -1; } - // create the block - b = PyBytes_FromStringAndSize(NULL, block_size); - if (b == NULL) { + if (PyBytesWriter_Grow(buffer->writer, block_size)) { PyErr_SetString(PyExc_MemoryError, unable_allocate_msg); return -1; } - if (PyList_Append(buffer->list, b) < 0) { - Py_DECREF(b); - return -1; - } - Py_DECREF(b); + + Py_ssize_t current_size = buffer->allocated; // set variables buffer->allocated += block_size; + buffer->num_blocks += 1; - *next_out = PyBytes_AS_STRING(b); + char *data = PyBytesWriter_GetData(buffer->writer); + *next_out = data + current_size; return block_size; } @@ -265,54 +242,20 @@ static inline PyObject * _BlocksOutputBuffer_Finish(_BlocksOutputBuffer *buffer, const Py_ssize_t avail_out) { - PyObject *result, *block; - const Py_ssize_t list_len = Py_SIZE(buffer->list); - - // fast path for single block - if ((list_len == 1 && avail_out == 0) || - (list_len == 2 && Py_SIZE(PyList_GET_ITEM(buffer->list, 1)) == avail_out)) - { - block = PyList_GET_ITEM(buffer->list, 0); - Py_INCREF(block); - - Py_CLEAR(buffer->list); - return block; - } - - // final bytes object - result = PyBytes_FromStringAndSize(NULL, buffer->allocated - avail_out); - if (result == NULL) { - PyErr_SetString(PyExc_MemoryError, unable_allocate_msg); - return NULL; - } - - // memory copy - if (list_len > 0) { - char *posi = PyBytes_AS_STRING(result); - - // blocks except the last one - Py_ssize_t i = 0; - for (; i < list_len-1; i++) { - block = PyList_GET_ITEM(buffer->list, i); - memcpy(posi, PyBytes_AS_STRING(block), Py_SIZE(block)); - posi += Py_SIZE(block); - } - // the last block - block = PyList_GET_ITEM(buffer->list, i); - memcpy(posi, PyBytes_AS_STRING(block), Py_SIZE(block) - avail_out); - } else { - assert(Py_SIZE(result) == 0); - } - - Py_CLEAR(buffer->list); - return result; + PyObject *obj; + assert(buffer->writer != NULL); + obj = PyBytesWriter_FinishWithSize(buffer->writer, + buffer->allocated - avail_out); + buffer->writer = NULL; + return obj; } /* Clean up the buffer when an error occurred. */ static inline void _BlocksOutputBuffer_OnError(_BlocksOutputBuffer *buffer) { - Py_CLEAR(buffer->list); + PyBytesWriter_Discard(buffer->writer); + buffer->writer = NULL; } #ifdef __cplusplus diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytes_methods.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytes_methods.h index 059dc2599..3e1474c1c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytes_methods.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytes_methods.h @@ -47,6 +47,9 @@ extern PyObject *_Py_bytes_endswith(const char *str, Py_ssize_t len, /* The maketrans() static method. */ extern PyObject* _Py_bytes_maketrans(Py_buffer *frm, Py_buffer *to); +/* Helper for repr(bytes) and repr(bytearray). */ +extern PyObject *_Py_bytes_repr(const char *, Py_ssize_t, int, const char *); + /* Shared __doc__ strings. */ extern const char _Py_isspace__doc__[]; extern const char _Py_isalpha__doc__[]; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytesobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytesobject.h index 8ea9b3ebb..177e6d101 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytesobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_bytesobject.h @@ -14,6 +14,11 @@ extern PyObject* _PyBytes_FormatEx( PyObject *args, int use_bytearray); +/* Concatenate two bytes objects. Used as the sq_concat slot and by the + * specializing interpreter. Unlike PyBytes_Concat(), this returns a new + * reference rather than modifying its first argument in place. */ +extern PyObject* _PyBytes_Concat(PyObject *a, PyObject *b); + extern PyObject* _PyBytes_FromHex( PyObject *string, int use_bytearray); @@ -60,88 +65,46 @@ PyAPI_FUNC(void) _PyBytes_Repeat(char* dest, Py_ssize_t len_dest, const char* src, Py_ssize_t len_src); -/* --- _PyBytesWriter ----------------------------------------------------- */ - -/* The _PyBytesWriter structure is big: it contains an embedded "stack buffer". - A _PyBytesWriter variable must be declared at the end of variables in a - function to optimize the memory allocation on the stack. */ -typedef struct { - /* bytes, bytearray or NULL (when the small buffer is used) */ - PyObject *buffer; +/* _PyBytesObject_SIZE gives the basic size of a bytes object; any memory allocation + for a bytes object of length n should request PyBytesObject_SIZE + n bytes. - /* Number of allocated size. */ - Py_ssize_t allocated; + Using _PyBytesObject_SIZE instead of sizeof(PyBytesObject) saves + 3 or 7 bytes per bytes object allocation on a typical system. +*/ +#define _PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1) - /* Minimum number of allocated bytes, - incremented by _PyBytesWriter_Prepare() */ - Py_ssize_t min_size; +/* --- PyBytesWriter ------------------------------------------------------ */ - /* If non-zero, use a bytearray instead of a bytes object for buffer. */ +struct PyBytesWriter { + char small_buffer[256]; + PyObject *obj; + Py_ssize_t size; int use_bytearray; - - /* If non-zero, overallocate the buffer (default: 0). - This flag must be zero if use_bytearray is non-zero. */ int overallocate; +}; + +// Export for '_testcapi' shared extension +PyAPI_FUNC(PyBytesWriter*) _PyBytesWriter_CreateByteArray(Py_ssize_t size); - /* Stack buffer */ - int use_small_buffer; - char small_buffer[512]; -} _PyBytesWriter; - -/* Initialize a bytes writer - - By default, the overallocation is disabled. Set the overallocate attribute - to control the allocation of the buffer. - - Export _PyBytesWriter API for '_pickle' shared extension. */ -PyAPI_FUNC(void) _PyBytesWriter_Init(_PyBytesWriter *writer); - -/* Get the buffer content and reset the writer. - Return a bytes object, or a bytearray object if use_bytearray is non-zero. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(PyObject *) _PyBytesWriter_Finish(_PyBytesWriter *writer, - void *str); - -/* Deallocate memory of a writer (clear its internal buffer). */ -PyAPI_FUNC(void) _PyBytesWriter_Dealloc(_PyBytesWriter *writer); - -/* Allocate the buffer to write size bytes. - Return the pointer to the beginning of buffer data. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(void*) _PyBytesWriter_Alloc(_PyBytesWriter *writer, - Py_ssize_t size); - -/* Ensure that the buffer is large enough to write *size* bytes. - Add size to the writer minimum size (min_size attribute). - - str is the current pointer inside the buffer. - Return the updated current pointer inside the buffer. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(void*) _PyBytesWriter_Prepare(_PyBytesWriter *writer, - void *str, - Py_ssize_t size); - -/* Resize the buffer to make it larger. - The new buffer may be larger than size bytes because of overallocation. - Return the updated current pointer inside the buffer. - Raise an exception and return NULL on error. - - Note: size must be greater than the number of allocated bytes in the writer. - - This function doesn't use the writer minimum size (min_size attribute). - - See also _PyBytesWriter_Prepare(). - */ -PyAPI_FUNC(void*) _PyBytesWriter_Resize(_PyBytesWriter *writer, - void *str, - Py_ssize_t size); - -/* Write bytes. - Raise an exception and return NULL on error. */ -PyAPI_FUNC(void*) _PyBytesWriter_WriteBytes(_PyBytesWriter *writer, - void *str, - const void *bytes, - Py_ssize_t size); +static inline Py_ssize_t +_PyBytesWriter_GetSize(PyBytesWriter *writer) +{ + return writer->size; +} + +static inline char* +_PyBytesWriter_GetData(PyBytesWriter *writer) +{ + if (writer->obj == NULL) { + return writer->small_buffer; + } + else if (writer->use_bytearray) { + return PyByteArray_AS_STRING(writer->obj); + } + else { + return PyBytes_AS_STRING(writer->obj); + } +} #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_call.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_call.h index 05b58b2ba..a9db8860e 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_call.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_call.h @@ -64,39 +64,6 @@ PyAPI_FUNC(PyObject*) _PyObject_CallMethod( PyObject *name, const char *format, ...); -extern PyObject* _PyObject_CallMethodIdObjArgs( - PyObject *obj, - _Py_Identifier *name, - ...); - -static inline PyObject * -_PyObject_VectorcallMethodId( - _Py_Identifier *name, PyObject *const *args, - size_t nargsf, PyObject *kwnames) -{ - PyObject *oname = _PyUnicode_FromId(name); /* borrowed */ - if (!oname) { - return _Py_NULL; - } - return PyObject_VectorcallMethod(oname, args, nargsf, kwnames); -} - -static inline PyObject * -_PyObject_CallMethodIdNoArgs(PyObject *self, _Py_Identifier *name) -{ - size_t nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; - return _PyObject_VectorcallMethodId(name, &self, nargsf, _Py_NULL); -} - -static inline PyObject * -_PyObject_CallMethodIdOneArg(PyObject *self, _Py_Identifier *name, PyObject *arg) -{ - PyObject *args[2] = {self, arg}; - size_t nargsf = 2 | PY_VECTORCALL_ARGUMENTS_OFFSET; - assert(arg != NULL); - return _PyObject_VectorcallMethodId(name, args, nargsf, _Py_NULL); -} - extern PyObject *_PyObject_VectorcallPrepend( PyThreadState *tstate, @@ -194,17 +161,18 @@ _PyObject_CallNoArgs(PyObject *func) { } -extern PyObject *const * +PyAPI_FUNC(PyObject *const *) _PyStack_UnpackDict(PyThreadState *tstate, PyObject *const *args, Py_ssize_t nargs, PyObject *kwargs, PyObject **p_kwnames); -extern void _PyStack_UnpackDict_Free( +// Exported for external JIT support +PyAPI_FUNC(void) _PyStack_UnpackDict_Free( PyObject *const *stack, Py_ssize_t nargs, PyObject *kwnames); -extern void _PyStack_UnpackDict_FreeNoDecRef( +PyAPI_FUNC(void) _PyStack_UnpackDict_FreeNoDecRef( PyObject *const *stack, PyObject *kwnames); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ceval.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ceval.h index e3fa8e10e..fd4221f08 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ceval.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_ceval.h @@ -33,8 +33,6 @@ extern int _PyEval_SetOpcodeTrace(PyFrameObject *f, bool enable); // Export for 'array' shared extension PyAPI_FUNC(PyObject*) _PyEval_GetBuiltin(PyObject *); -extern PyObject* _PyEval_GetBuiltinId(_Py_Identifier *); - extern void _PyEval_SetSwitchInterval(unsigned long microseconds); extern unsigned long _PyEval_GetSwitchInterval(void); @@ -96,7 +94,7 @@ typedef struct { void* (*init_state)(void); // Callback to register every trampoline being created void (*write_state)(void* state, const void *code_addr, - unsigned int code_size, PyCodeObject* code); + size_t code_size, PyCodeObject* code); // Callback to free the trampoline state int (*free_state)(void* state); } _PyPerf_Callbacks; @@ -110,6 +108,10 @@ extern PyStatus _PyPerfTrampoline_AfterFork_Child(void); #ifdef PY_HAVE_PERF_TRAMPOLINE extern _PyPerf_Callbacks _Py_perfmap_callbacks; extern _PyPerf_Callbacks _Py_perfmap_jit_callbacks; +extern void _PyPerfJit_WriteNamedCode(const void *code_addr, + size_t code_size, + const char *entry, + const char *filename); #endif static inline PyObject* @@ -122,6 +124,15 @@ _PyEval_EvalFrame(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwfl return tstate->interp->eval_frame(tstate, frame, throwflag); } +#ifdef _Py_TIER2 +_Py_CODEUNIT *_PyTier2Interpreter( + struct _PyExecutorObject *current_executor, _PyInterpreterFrame *frame, + _PyStackRef *stack_pointer, PyThreadState *tstate +); +#endif + +extern _PyJitEntryFuncPtr _Py_jit_entry; + extern PyObject* _PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func, PyObject *locals, @@ -216,7 +227,7 @@ PyAPI_FUNC(int) _Py_CheckRecursiveCall( PyThreadState *tstate, const char *where); -int _Py_CheckRecursiveCallPy( +PyAPI_FUNC(int) _Py_CheckRecursiveCallPy( PyThreadState *tstate); static inline int _Py_EnterRecursiveCallTstate(PyThreadState *tstate, @@ -235,16 +246,12 @@ static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) { PyAPI_FUNC(void) _Py_InitializeRecursionLimits(PyThreadState *tstate); -static inline int _Py_ReachedRecursionLimit(PyThreadState *tstate) { - uintptr_t here_addr = _Py_get_machine_stack_pointer(); - _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate; - assert(_tstate->c_stack_hard_limit != 0); -#if _Py_STACK_GROWS_DOWN - return here_addr <= _tstate->c_stack_soft_limit; -#else - return here_addr >= _tstate->c_stack_soft_limit; -#endif -} +PyAPI_FUNC(int) _Py_ReachedRecursionLimit(PyThreadState *tstate); + +// Export for test_peg_generator +PyAPI_FUNC(int) _Py_ReachedRecursionLimitWithMargin( + PyThreadState *tstate, + int margin_count); static inline void _Py_LeaveRecursiveCall(void) { } @@ -267,6 +274,9 @@ PyAPI_FUNC(PyObject *)_Py_MakeCoro(PyFunctionObject *func); and asynchronous exception */ PyAPI_FUNC(int) _Py_HandlePending(PyThreadState *tstate); +/* Raise exception set by PyThreadState_SetAsyncExc, if any */ +PyAPI_FUNC(int) _PyEval_RaiseAsyncExc(PyThreadState *tstate); + extern PyObject * _PyEval_GetFrameLocals(void); typedef PyObject *(*conversion_func)(PyObject *); @@ -289,13 +299,25 @@ PyAPI_FUNC(int) _PyEval_ExceptionGroupMatch(_PyInterpreterFrame *, PyObject* exc PyAPI_FUNC(void) _PyEval_FormatAwaitableError(PyThreadState *tstate, PyTypeObject *type, int oparg); PyAPI_FUNC(void) _PyEval_FormatExcCheckArg(PyThreadState *tstate, PyObject *exc, const char *format_str, PyObject *obj); PyAPI_FUNC(void) _PyEval_FormatExcUnbound(PyThreadState *tstate, PyCodeObject *co, int oparg); -PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs); +PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs, PyObject *dupkey); PyAPI_FUNC(PyObject *) _PyEval_ImportFrom(PyThreadState *, PyObject *, PyObject *); -PyAPI_FUNC(PyObject *) _PyEval_ImportName(PyThreadState *, _PyInterpreterFrame *, PyObject *, PyObject *, PyObject *); + +PyAPI_FUNC(PyObject *) _PyEval_LazyImportName( + PyThreadState *tstate, PyObject *builtins, PyObject *globals, + PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level, + int lazy); +PyAPI_FUNC(PyObject *) _PyEval_LazyImportFrom( + PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *v, PyObject *name); +PyAPI_FUNC(PyObject *) _PyEval_ImportName( + PyThreadState *tstate, PyObject *builtins, PyObject *globals, + PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level); +PyObject * _PyEval_ImportNameWithImport( + PyThreadState *tstate, PyObject *import_func, PyObject *globals, + PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level); PyAPI_FUNC(PyObject *)_PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, Py_ssize_t nargs, PyObject *kwargs); PyAPI_FUNC(PyObject *)_PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys); PyAPI_FUNC(void) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr); -PyAPI_FUNC(bool) _PyEval_NoToolsForUnwind(PyThreadState *tstate); +PyAPI_FUNC(bool) _PyEval_NoToolsForUnwind(PyThreadState *tstate, _PyInterpreterFrame *frame); PyAPI_FUNC(int) _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v, int argcnt, int argcntafter, _PyStackRef *sp); PyAPI_FUNC(void) _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame); PyAPI_FUNC(PyObject **) _PyObjectArray_FromStackRefArray(_PyStackRef *input, Py_ssize_t nargs, PyObject **scratch); @@ -308,6 +330,7 @@ PyAPI_FUNC(PyObject *) _PyEval_GetAwaitable(PyObject *iterable, int oparg); PyAPI_FUNC(PyObject *) _PyEval_LoadName(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *name); PyAPI_FUNC(int) _Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args); +PyAPI_FUNC(_PyStackRef) _PyEval_GetIter(_PyStackRef iterable, _PyStackRef *null_or_index, int yield_from); /* * Indicate whether a special method of given 'oparg' can use the (improved) @@ -357,8 +380,6 @@ _Py_eval_breaker_bit_is_set(PyThreadState *tstate, uintptr_t bit) void _Py_set_eval_breaker_bit_all(PyInterpreterState *interp, uintptr_t bit); void _Py_unset_eval_breaker_bit_all(PyInterpreterState *interp, uintptr_t bit); -PyAPI_FUNC(_PyStackRef) _PyFloat_FromDouble_ConsumeInputs(_PyStackRef left, _PyStackRef right, double value); - #ifndef Py_SUPPORTS_REMOTE_DEBUG #if defined(__APPLE__) #include @@ -376,6 +397,9 @@ PyAPI_FUNC(_PyStackRef) _PyFloat_FromDouble_ConsumeInputs(_PyStackRef left, _PyS extern int _PyRunRemoteDebugger(PyThreadState *tstate); #endif +PyAPI_FUNC(_PyStackRef) +_PyForIter_VirtualIteratorNext(PyThreadState* tstate, struct _PyInterpreterFrame* frame, _PyStackRef iter, _PyStackRef *index_ptr); + /* Special methods used by LOAD_SPECIAL */ #define SPECIAL___ENTER__ 0 #define SPECIAL___EXIT__ 1 @@ -383,11 +407,87 @@ extern int _PyRunRemoteDebugger(PyThreadState *tstate); #define SPECIAL___AEXIT__ 3 #define SPECIAL_MAX 3 +PyAPI_DATA(const _Py_CODEUNIT *) _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR; + +/* Helper functions for large uops */ + +PyAPI_FUNC(PyObject *) +_Py_VectorCall_StackRefSteal( + _PyStackRef callable, + _PyStackRef *arguments, + int total_args, + _PyStackRef kwnames); + +PyAPI_FUNC(PyObject*) +_Py_VectorCallInstrumentation_StackRefSteal( + _PyStackRef callable, + _PyStackRef* arguments, + int total_args, + _PyStackRef kwnames, + bool call_instrumentation, + _PyInterpreterFrame* frame, + _Py_CODEUNIT* this_instr, + PyThreadState* tstate); + +PyAPI_FUNC(PyObject *) +_Py_BuiltinCallFast_StackRef( + _PyStackRef callable, + _PyStackRef *arguments, + int total_args); + +PyAPI_FUNC(PyObject *) +_Py_BuiltinCallFastWithKeywords_StackRef( + _PyStackRef callable, + _PyStackRef *arguments, + int total_args); + +PyAPI_FUNC(PyObject *) +_PyCallMethodDescriptorFast_StackRef( + _PyStackRef callable, + PyCFunctionFast cfunc, + PyObject *self, + _PyStackRef *arguments, + int total_args); + +PyAPI_FUNC(PyObject *) +_PyCallMethodDescriptorFastWithKeywords_StackRef( + _PyStackRef callable, + PyCFunctionFastWithKeywords cfunc, + PyObject *self, + _PyStackRef *arguments, + int total_args); + +PyAPI_FUNC(PyObject *) +_Py_CallBuiltinClass_StackRef( + _PyStackRef callable, + _PyStackRef *arguments, + int total_args); + +PyAPI_FUNC(PyObject *) +_Py_BuildString_StackRefSteal( + _PyStackRef *arguments, + int total_args); + +PyAPI_FUNC(PyObject *) +_Py_BuildMap_StackRefSteal( + _PyStackRef *arguments, + int half_args); + +PyAPI_FUNC(void) +_Py_assert_within_stack_bounds( + _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, + const char *filename, int lineno); + PyAPI_FUNC(_PyStackRef) _Py_LoadAttr_StackRefSteal( PyThreadState *tstate, _PyStackRef owner, PyObject *name, _PyStackRef *self_or_null); +// Like PyMapping_GetOptionalItem, but returns the PyObject* instead of taking +// it as an out parameter. This helps MSVC's escape analysis when used with +// tail calling. +PyAPI_FUNC(PyObject*) _PyMapping_GetOptionalItem2(PyObject* obj, PyObject* key, int* err); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_code.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_code.h index dc0ea9c66..32242f89b 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_code.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_code.h @@ -141,6 +141,12 @@ typedef struct { #define INLINE_CACHE_ENTRIES_FOR_ITER CACHE_ENTRIES(_PyForIterCache) +typedef struct { + _Py_BackoffCounter counter; +} _PyGetIterCache; + +#define INLINE_CACHE_ENTRIES_GET_ITER CACHE_ENTRIES(_PyGetIterCache) + typedef struct { _Py_BackoffCounter counter; } _PySendCache; @@ -160,6 +166,12 @@ typedef struct { #define INLINE_CACHE_ENTRIES_CONTAINS_OP CACHE_ENTRIES(_PyContainsOpCache) +typedef struct { + _Py_BackoffCounter counter; +} _PyCallFunctionExCache; + +#define INLINE_CACHE_ENTRIES_CALL_FUNCTION_EX CACHE_ENTRIES(_PyCallFunctionExCache) + /* "Locals plus" for a code object is the set of locals + cell vars + * free vars. This relates to variable names as well as offsets into * the "fast locals" storage array of execution frames. The compiler @@ -262,7 +274,7 @@ extern PyObject* _PyCode_GetFreevars(PyCodeObject *); extern PyObject* _PyCode_GetCode(PyCodeObject *); /** API for initializing the line number tables. */ -extern int _PyCode_InitAddressRange(PyCodeObject* co, PyCodeAddressRange *bounds); +PyAPI_FUNC(int) _PyCode_InitAddressRange(PyCodeObject* co, PyCodeAddressRange *bounds); /** Out of process API for initializing the location table. */ extern void _PyLineTable_InitAddressRange( @@ -272,7 +284,7 @@ extern void _PyLineTable_InitAddressRange( PyCodeAddressRange *range); /** API for traversing the line number table. */ -extern int _PyLineTable_NextAddressRange(PyCodeAddressRange *range); +PyAPI_FUNC(int) _PyLineTable_NextAddressRange(PyCodeAddressRange *range); extern int _PyLineTable_PreviousAddressRange(PyCodeAddressRange *range); // Similar to PyCode_Addr2Line(), but return -1 if the code object is invalid @@ -286,45 +298,39 @@ extern int _PyCode_SafeAddr2Line(PyCodeObject *co, int addr); extern void _PyCode_Clear_Executors(PyCodeObject *code); -#ifdef Py_GIL_DISABLED -// gh-115999 tracks progress on addressing this. -#define ENABLE_SPECIALIZATION 0 -// Use this to enable specialization families once they are thread-safe. All -// uses will be replaced with ENABLE_SPECIALIZATION once all families are -// thread-safe. -#define ENABLE_SPECIALIZATION_FT 1 -#else #define ENABLE_SPECIALIZATION 1 -#define ENABLE_SPECIALIZATION_FT ENABLE_SPECIALIZATION -#endif -/* Specialization functions */ +/* Specialization functions, these are exported only for other re-generated + * interpreters to call */ -extern void _Py_Specialize_LoadSuperAttr(_PyStackRef global_super, _PyStackRef cls, +PyAPI_FUNC(void) _Py_Specialize_LoadSuperAttr(_PyStackRef global_super, _PyStackRef cls, _Py_CODEUNIT *instr, int load_method); -extern void _Py_Specialize_LoadAttr(_PyStackRef owner, _Py_CODEUNIT *instr, +PyAPI_FUNC(void) _Py_Specialize_LoadAttr(_PyStackRef owner, _Py_CODEUNIT *instr, PyObject *name); -extern void _Py_Specialize_StoreAttr(_PyStackRef owner, _Py_CODEUNIT *instr, +PyAPI_FUNC(void) _Py_Specialize_StoreAttr(_PyStackRef owner, _Py_CODEUNIT *instr, PyObject *name); -extern void _Py_Specialize_LoadGlobal(PyObject *globals, PyObject *builtins, +PyAPI_FUNC(void) _Py_Specialize_LoadGlobal(PyObject *globals, PyObject *builtins, _Py_CODEUNIT *instr, PyObject *name); -extern void _Py_Specialize_StoreSubscr(_PyStackRef container, _PyStackRef sub, +PyAPI_FUNC(void) _Py_Specialize_StoreSubscr(_PyStackRef container, _PyStackRef sub, _Py_CODEUNIT *instr); -extern void _Py_Specialize_Call(_PyStackRef callable, _Py_CODEUNIT *instr, - int nargs); -extern void _Py_Specialize_CallKw(_PyStackRef callable, _Py_CODEUNIT *instr, +PyAPI_FUNC(void) _Py_Specialize_Call(_PyStackRef callable, _PyStackRef self_or_null, + _Py_CODEUNIT *instr, int nargs); +PyAPI_FUNC(void) _Py_Specialize_CallKw(_PyStackRef callable, _Py_CODEUNIT *instr, int nargs); -extern void _Py_Specialize_BinaryOp(_PyStackRef lhs, _PyStackRef rhs, _Py_CODEUNIT *instr, +PyAPI_FUNC(void) _Py_Specialize_BinaryOp(_PyStackRef lhs, _PyStackRef rhs, _Py_CODEUNIT *instr, int oparg, _PyStackRef *locals); -extern void _Py_Specialize_CompareOp(_PyStackRef lhs, _PyStackRef rhs, +PyAPI_FUNC(void) _Py_Specialize_CompareOp(_PyStackRef lhs, _PyStackRef rhs, _Py_CODEUNIT *instr, int oparg); -extern void _Py_Specialize_UnpackSequence(_PyStackRef seq, _Py_CODEUNIT *instr, +PyAPI_FUNC(void) _Py_Specialize_UnpackSequence(_PyStackRef seq, _Py_CODEUNIT *instr, int oparg); -extern void _Py_Specialize_ForIter(_PyStackRef iter, _Py_CODEUNIT *instr, int oparg); -extern void _Py_Specialize_Send(_PyStackRef receiver, _Py_CODEUNIT *instr); -extern void _Py_Specialize_ToBool(_PyStackRef value, _Py_CODEUNIT *instr); -extern void _Py_Specialize_ContainsOp(_PyStackRef value, _Py_CODEUNIT *instr); -extern void _Py_GatherStats_GetIter(_PyStackRef iterable); +PyAPI_FUNC(void) _Py_Specialize_ForIter(_PyStackRef iter, _PyStackRef null_or_index, _Py_CODEUNIT *instr, int oparg); +PyAPI_FUNC(void) _Py_Specialize_Send(_PyStackRef receiver, _Py_CODEUNIT *instr); +PyAPI_FUNC(void) _Py_Specialize_ToBool(_PyStackRef value, _Py_CODEUNIT *instr); +PyAPI_FUNC(void) _Py_Specialize_ContainsOp(_PyStackRef value, _Py_CODEUNIT *instr); +PyAPI_FUNC(void) _Py_GatherStats_GetIter(_PyStackRef iterable); +PyAPI_FUNC(void) _Py_Specialize_CallFunctionEx(_PyStackRef func_st, _Py_CODEUNIT *instr); +PyAPI_FUNC(void) _Py_Specialize_Resume(_Py_CODEUNIT *instr, PyThreadState *tstate, _PyInterpreterFrame *frame); +PyAPI_FUNC(void) _Py_Specialize_GetIter(_PyStackRef iterable, _Py_CODEUNIT *instr); // Utility functions for reading/writing 32/64-bit values in the inline caches. // Great care should be taken to ensure that these functions remain correct and @@ -458,6 +464,9 @@ write_location_entry_start(uint8_t *ptr, int code, int length) #define ADAPTIVE_COOLDOWN_BACKOFF 0 // Can't assert this in pycore_backoff.h because of header order dependencies +#if JUMP_BACKWARD_INITIAL_VALUE <= ADAPTIVE_COOLDOWN_VALUE +# error "JIT threshold value should be larger than adaptive cooldown value" +#endif #if SIDE_EXIT_INITIAL_VALUE <= ADAPTIVE_COOLDOWN_VALUE # error "Cold exit value should be larger than adaptive cooldown value" #endif @@ -494,6 +503,18 @@ typedef struct { int oparg; binaryopguardfunc guard; binaryopactionfunc action; + /* Static type of the result, or NULL if unknown. Used by the tier 2 + optimizer to propagate type information through _BINARY_OP_EXTEND. */ + PyTypeObject *result_type; + /* Nonzero iff `action` always returns a freshly allocated object (not + aliased to either operand). Used by the tier 2 optimizer to enable + inplace follow-up ops. */ + int result_unique; + /* Expected types of the left and right operands. Used by the tier 2 + optimizer to eliminate _GUARD_BINARY_OP_EXTEND when the operand + types are already known. NULL means unknown/don't eliminate. */ + PyTypeObject *lhs_type; + PyTypeObject *rhs_type; } _PyBinaryOpSpecializationDescr; /* Comparison bit masks. */ @@ -516,9 +537,10 @@ typedef struct { #define COMPARISON_NOT_EQUALS (COMPARISON_UNORDERED | COMPARISON_LESS_THAN | COMPARISON_GREATER_THAN) -extern int _Py_Instrument(PyCodeObject *co, PyInterpreterState *interp); +PyAPI_FUNC(int) _Py_Instrument(PyCodeObject *co, PyInterpreterState *interp); -extern _Py_CODEUNIT _Py_GetBaseCodeUnit(PyCodeObject *code, int offset); +// Export for '_testinternalcapi' shared extension +PyAPI_FUNC(_Py_CODEUNIT) _Py_GetBaseCodeUnit(PyCodeObject *code, int offset); extern int _PyInstruction_GetLength(PyCodeObject *code, int offset); @@ -552,7 +574,7 @@ _PyCode_GetTLBCFast(PyThreadState *tstate, PyCodeObject *co) // Return a pointer to the thread-local bytecode for the current thread, // creating it if necessary. -extern _Py_CODEUNIT *_PyCode_GetTLBC(PyCodeObject *co); +PyAPI_FUNC(_Py_CODEUNIT *) _PyCode_GetTLBC(PyCodeObject *co); // Reserve an index for the current thread into thread-local bytecode // arrays @@ -560,6 +582,10 @@ extern _Py_CODEUNIT *_PyCode_GetTLBC(PyCodeObject *co); // Returns the reserved index or -1 on error. extern int32_t _Py_ReserveTLBCIndex(PyInterpreterState *interp); +// Release an index returned by _Py_ReserveTLBCIndex() that was never stored +// in a PyThreadState. +extern void _Py_UnreserveTLBCIndex(PyInterpreterState *interp, int32_t index); + // Release the current thread's index into thread-local bytecode arrays extern void _Py_ClearTLBCIndex(_PyThreadStateImpl *tstate); @@ -664,6 +690,15 @@ PyAPI_FUNC(int) _PyCode_VerifyStateless( PyAPI_FUNC(int) _PyCode_CheckPureFunction(PyCodeObject *, const char **); PyAPI_FUNC(int) _PyCode_ReturnsOnlyNone(PyCodeObject *); +/* Create a comparable key used to compare constants taking in account the + * object type. It is used to make sure types are not coerced (e.g., float and + * complex) _and_ to distinguish 0.0 from -0.0 e.g. on IEEE platforms + * + * Return (type(obj), obj, ...): a tuple with variable size (at least 2 items) + * depending on the type and the value. The type is the first item to not + * compare bytes and str which can raise a BytesWarning exception. */ +extern PyObject* _PyCode_ConstantKey(PyObject *obj); + #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_compile.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_compile.h index ed776bbb7..911cc1f10 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_compile.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_compile.h @@ -32,7 +32,8 @@ PyAPI_FUNC(PyCodeObject*) _PyAST_Compile( PyObject *filename, PyCompilerFlags *flags, int optimize, - struct _arena *arena); + struct _arena *arena, + PyObject *module); /* AST preprocessing */ extern int _PyCompile_AstPreprocess( @@ -41,7 +42,8 @@ extern int _PyCompile_AstPreprocess( PyCompilerFlags *flags, int optimize, struct _arena *arena, - int syntax_check_only); + int syntax_check_only, + PyObject *module); extern int _PyAST_Preprocess( struct _mod *, @@ -50,7 +52,8 @@ extern int _PyAST_Preprocess( int optimize, int ff_features, int syntax_check_only, - int enable_warnings); + int enable_warnings, + PyObject *module); typedef struct { @@ -96,6 +99,7 @@ typedef enum { enum _PyCompile_FBlockType { COMPILE_FBLOCK_WHILE_LOOP, COMPILE_FBLOCK_FOR_LOOP, + COMPILE_FBLOCK_ASYNC_FOR_LOOP, COMPILE_FBLOCK_TRY_EXCEPT, COMPILE_FBLOCK_FINALLY_TRY, COMPILE_FBLOCK_FINALLY_END, @@ -127,6 +131,7 @@ int _PyCompile_PushFBlock(struct _PyCompiler *c, _Py_SourceLocation loc, void _PyCompile_PopFBlock(struct _PyCompiler *c, enum _PyCompile_FBlockType t, _PyJumpTargetLabel block_label); _PyCompile_FBlockInfo *_PyCompile_TopFBlock(struct _PyCompiler *c); +bool _PyCompile_InExceptionHandler(struct _PyCompiler *c); int _PyCompile_EnterScope(struct _PyCompiler *c, identifier name, int scope_type, void *key, int lineno, PyObject *private, diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_context.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_context.h index c77ef7910..a833f790a 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_context.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_context.h @@ -55,5 +55,8 @@ struct _pycontexttokenobject { // Export for '_testcapi' shared extension PyAPI_FUNC(PyObject*) _PyContext_NewHamtForTests(void); +PyAPI_FUNC(int) _PyContext_Enter(PyThreadState *ts, PyObject *octx); +PyAPI_FUNC(int) _PyContext_Exit(PyThreadState *ts, PyObject *octx); + #endif /* !Py_INTERNAL_CONTEXT_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_critical_section.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_critical_section.h index 942bcff7e..51d99d74c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_critical_section.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_critical_section.h @@ -32,7 +32,7 @@ extern "C" { const bool _should_lock_cs = PyList_CheckExact(_orig_seq); \ PyCriticalSection _cs; \ if (_should_lock_cs) { \ - _PyCriticalSection_Begin(&_cs, _orig_seq); \ + PyCriticalSection_Begin(&_cs, _orig_seq); \ } # define Py_END_CRITICAL_SECTION_SEQUENCE_FAST() \ @@ -50,11 +50,20 @@ extern "C" { // Asserts that the mutex for the given object is locked. The mutex must // be held by the top-most critical section otherwise there's the // possibility that the mutex would be swalled out in some code paths. +// +// NOTE: We use Py_REFCNT(op) != 1 instead of +// !PyUnstable_Object_IsUniquelyReferenced(op) because the free threading +// GC can change an object's ob_tid (it overwrites ob_tid and later +// restores it from the mimalloc segment). This means +// PyUnstable_Object_IsUniquelyReferenced() may spuriously return false +// after a GC collection, even though the thread may still have exclusive +// access to the object. The refcount check is a looser but still catches +// most misuses. #ifdef Py_DEBUG # define _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op) \ if (Py_REFCNT(op) != 1) { \ - _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&_PyObject_CAST(op)->ob_mutex); \ + _PyCriticalSection_AssertHeldObj(_PyObject_CAST(op)); \ } #else /* Py_DEBUG */ @@ -77,10 +86,10 @@ _PyCriticalSection_Resume(PyThreadState *tstate); // (private) slow path for locking the mutex PyAPI_FUNC(void) -_PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m); +_PyCriticalSection_BeginSlow(PyThreadState *tstate, PyCriticalSection *c, PyMutex *m); PyAPI_FUNC(void) -_PyCriticalSection2_BeginSlow(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2, +_PyCriticalSection2_BeginSlow(PyThreadState *tstate, PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2, int is_m1_locked); PyAPI_FUNC(void) @@ -95,34 +104,30 @@ _PyCriticalSection_IsActive(uintptr_t tag) } static inline void -_PyCriticalSection_BeginMutex(PyCriticalSection *c, PyMutex *m) +_PyCriticalSection_BeginMutex(PyThreadState *tstate, PyCriticalSection *c, PyMutex *m) { if (PyMutex_LockFast(m)) { - PyThreadState *tstate = _PyThreadState_GET(); c->_cs_mutex = m; c->_cs_prev = tstate->critical_section; tstate->critical_section = (uintptr_t)c; } else { - _PyCriticalSection_BeginSlow(c, m); + _PyCriticalSection_BeginSlow(tstate, c, m); } } -#define PyCriticalSection_BeginMutex _PyCriticalSection_BeginMutex static inline void -_PyCriticalSection_Begin(PyCriticalSection *c, PyObject *op) +_PyCriticalSection_Begin(PyThreadState *tstate, PyCriticalSection *c, PyObject *op) { - _PyCriticalSection_BeginMutex(c, &op->ob_mutex); + _PyCriticalSection_BeginMutex(tstate, c, &op->ob_mutex); } -#define PyCriticalSection_Begin _PyCriticalSection_Begin // Removes the top-most critical section from the thread's stack of critical // sections. If the new top-most critical section is inactive, then it is // resumed. static inline void -_PyCriticalSection_Pop(PyCriticalSection *c) +_PyCriticalSection_Pop(PyThreadState *tstate, PyCriticalSection *c) { - PyThreadState *tstate = _PyThreadState_GET(); uintptr_t prev = c->_cs_prev; tstate->critical_section = prev; @@ -132,7 +137,7 @@ _PyCriticalSection_Pop(PyCriticalSection *c) } static inline void -_PyCriticalSection_End(PyCriticalSection *c) +_PyCriticalSection_End(PyThreadState *tstate, PyCriticalSection *c) { // If the mutex is NULL, we used the fast path in // _PyCriticalSection_BeginSlow for locks already held in the top-most @@ -141,18 +146,17 @@ _PyCriticalSection_End(PyCriticalSection *c) return; } PyMutex_Unlock(c->_cs_mutex); - _PyCriticalSection_Pop(c); + _PyCriticalSection_Pop(tstate, c); } -#define PyCriticalSection_End _PyCriticalSection_End static inline void -_PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2) +_PyCriticalSection2_BeginMutex(PyThreadState *tstate, PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2) { if (m1 == m2) { // If the two mutex arguments are the same, treat this as a critical // section with a single mutex. c->_cs_mutex2 = NULL; - _PyCriticalSection_BeginMutex(&c->_cs_base, m1); + _PyCriticalSection_BeginMutex(tstate, &c->_cs_base, m1); return; } @@ -167,7 +171,6 @@ _PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2) if (PyMutex_LockFast(m1)) { if (PyMutex_LockFast(m2)) { - PyThreadState *tstate = _PyThreadState_GET(); c->_cs_base._cs_mutex = m1; c->_cs_mutex2 = m2; c->_cs_base._cs_prev = tstate->critical_section; @@ -176,24 +179,22 @@ _PyCriticalSection2_BeginMutex(PyCriticalSection2 *c, PyMutex *m1, PyMutex *m2) tstate->critical_section = p; } else { - _PyCriticalSection2_BeginSlow(c, m1, m2, 1); + _PyCriticalSection2_BeginSlow(tstate, c, m1, m2, 1); } } else { - _PyCriticalSection2_BeginSlow(c, m1, m2, 0); + _PyCriticalSection2_BeginSlow(tstate, c, m1, m2, 0); } } -#define PyCriticalSection2_BeginMutex _PyCriticalSection2_BeginMutex static inline void -_PyCriticalSection2_Begin(PyCriticalSection2 *c, PyObject *a, PyObject *b) +_PyCriticalSection2_Begin(PyThreadState *tstate, PyCriticalSection2 *c, PyObject *a, PyObject *b) { - _PyCriticalSection2_BeginMutex(c, &a->ob_mutex, &b->ob_mutex); + _PyCriticalSection2_BeginMutex(tstate, c, &a->ob_mutex, &b->ob_mutex); } -#define PyCriticalSection2_Begin _PyCriticalSection2_Begin static inline void -_PyCriticalSection2_End(PyCriticalSection2 *c) +_PyCriticalSection2_End(PyThreadState *tstate, PyCriticalSection2 *c) { // if mutex1 is NULL, we used the fast path in either // _PyCriticalSection_BeginSlow or _PyCriticalSection2_BeginSlow for mutexes @@ -206,9 +207,8 @@ _PyCriticalSection2_End(PyCriticalSection2 *c) PyMutex_Unlock(c->_cs_mutex2); } PyMutex_Unlock(c->_cs_base._cs_mutex); - _PyCriticalSection_Pop(&c->_cs_base); + _PyCriticalSection_Pop(tstate, &c->_cs_base); } -#define PyCriticalSection2_End _PyCriticalSection2_End static inline void _PyCriticalSection_AssertHeld(PyMutex *mutex) @@ -228,6 +228,67 @@ _PyCriticalSection_AssertHeld(PyMutex *mutex) #endif } +static inline void +_PyCriticalSection_AssertHeldObj(PyObject *op) +{ +#ifdef Py_DEBUG + PyMutex *mutex = &_PyObject_CAST(op)->ob_mutex; + PyThreadState *tstate = _PyThreadState_GET(); + uintptr_t prev = tstate->critical_section; + if (prev & _Py_CRITICAL_SECTION_TWO_MUTEXES) { + PyCriticalSection2 *cs = (PyCriticalSection2 *)(prev & ~_Py_CRITICAL_SECTION_MASK); + _PyObject_ASSERT_WITH_MSG(op, + (cs != NULL && (cs->_cs_base._cs_mutex == mutex || cs->_cs_mutex2 == mutex)), + "Critical section of object is not held"); + } + else { + PyCriticalSection *cs = (PyCriticalSection *)(prev & ~_Py_CRITICAL_SECTION_MASK); + _PyObject_ASSERT_WITH_MSG(op, + (cs != NULL && cs->_cs_mutex == mutex), + "Critical section of object is not held"); + } + +#endif +} + +#undef Py_BEGIN_CRITICAL_SECTION +# define Py_BEGIN_CRITICAL_SECTION(op) \ + { \ + PyCriticalSection _py_cs; \ + PyThreadState *_cs_tstate = _PyThreadState_GET(); \ + _PyCriticalSection_Begin(_cs_tstate, &_py_cs, _PyObject_CAST(op)) + +#undef Py_BEGIN_CRITICAL_SECTION_MUTEX +# define Py_BEGIN_CRITICAL_SECTION_MUTEX(mutex) \ + { \ + PyCriticalSection _py_cs; \ + PyThreadState *_cs_tstate = _PyThreadState_GET(); \ + _PyCriticalSection_BeginMutex(_cs_tstate, &_py_cs, mutex) + +#undef Py_END_CRITICAL_SECTION +# define Py_END_CRITICAL_SECTION() \ + _PyCriticalSection_End(_cs_tstate, &_py_cs); \ + } + +#undef Py_BEGIN_CRITICAL_SECTION2 +# define Py_BEGIN_CRITICAL_SECTION2(a, b) \ + { \ + PyCriticalSection2 _py_cs2; \ + PyThreadState *_cs_tstate = _PyThreadState_GET(); \ + _PyCriticalSection2_Begin(_cs_tstate, &_py_cs2, _PyObject_CAST(a), _PyObject_CAST(b)) + +#undef Py_BEGIN_CRITICAL_SECTION2_MUTEX +# define Py_BEGIN_CRITICAL_SECTION2_MUTEX(m1, m2) \ + { \ + PyCriticalSection2 _py_cs2; \ + PyThreadState *_cs_tstate = _PyThreadState_GET(); \ + _PyCriticalSection2_BeginMutex(_cs_tstate, &_py_cs2, m1, m2) + +#undef Py_END_CRITICAL_SECTION2 +# define Py_END_CRITICAL_SECTION2() \ + _PyCriticalSection2_End(_cs_tstate, &_py_cs2); \ + } + #endif /* Py_GIL_DISABLED */ #ifdef __cplusplus diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_crossinterp.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_crossinterp.h index 81faffac1..bed966681 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_crossinterp.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_crossinterp.h @@ -265,6 +265,12 @@ typedef struct { // heap types PyObject *PyExc_NotShareableError; } exceptions; + + // Cached references to pickle.dumps/loads (per-interpreter). + struct { + PyObject *dumps; + PyObject *loads; + } pickle; } _PyXI_state_t; #define _PyXI_GET_GLOBAL_STATE(interp) (&(interp)->runtime->xi) diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_debug_offsets.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_debug_offsets.h index 8e7cd16ac..6e1eb573c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_debug_offsets.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_debug_offsets.h @@ -102,12 +102,24 @@ typedef struct _Py_DebugOffsets { uint64_t next; uint64_t interp; uint64_t current_frame; + uint64_t base_frame; + uint64_t last_profiled_frame; + uint64_t last_profiled_frame_seq; uint64_t thread_id; uint64_t native_thread_id; uint64_t datastack_chunk; uint64_t status; + uint64_t holds_gil; + uint64_t gil_requested; + uint64_t current_exception; + uint64_t exc_state; } thread_state; + // Exception stack item offset + struct { + uint64_t exc_value; + } err_stackitem; + // InterpreterFrame offset; struct _interpreter_frame { uint64_t size; @@ -147,8 +159,16 @@ typedef struct _Py_DebugOffsets { uint64_t tp_name; uint64_t tp_repr; uint64_t tp_flags; + uint64_t tp_basicsize; + uint64_t tp_dictoffset; } type_object; + // PyHeapTypeObject offset; + struct _heap_type_object { + uint64_t size; + uint64_t ht_cached_keys; + } heap_type_object; + // PyTuple object offset; struct _tuple_object { uint64_t size; @@ -204,12 +224,16 @@ typedef struct _Py_DebugOffsets { uint64_t state; uint64_t length; uint64_t asciiobject_size; + uint64_t compactunicodeobject_size; } unicode_object; // GC runtime state offset; struct _gc { uint64_t size; uint64_t collecting; + uint64_t frame; + uint64_t generation_stats_size; + uint64_t generation_stats; } gc; // Generator object offset; @@ -269,10 +293,20 @@ typedef struct _Py_DebugOffsets { .next = offsetof(PyThreadState, next), \ .interp = offsetof(PyThreadState, interp), \ .current_frame = offsetof(PyThreadState, current_frame), \ + .base_frame = offsetof(PyThreadState, base_frame), \ + .last_profiled_frame = offsetof(PyThreadState, last_profiled_frame), \ + .last_profiled_frame_seq = offsetof(PyThreadState, last_profiled_frame_seq), \ .thread_id = offsetof(PyThreadState, thread_id), \ .native_thread_id = offsetof(PyThreadState, native_thread_id), \ .datastack_chunk = offsetof(PyThreadState, datastack_chunk), \ .status = offsetof(PyThreadState, _status), \ + .holds_gil = offsetof(PyThreadState, holds_gil), \ + .gil_requested = offsetof(PyThreadState, gil_requested), \ + .current_exception = offsetof(PyThreadState, current_exception), \ + .exc_state = offsetof(PyThreadState, exc_state), \ + }, \ + .err_stackitem = { \ + .exc_value = offsetof(_PyErr_StackItem, exc_value), \ }, \ .interpreter_frame = { \ .size = sizeof(_PyInterpreterFrame), \ @@ -306,6 +340,12 @@ typedef struct _Py_DebugOffsets { .tp_name = offsetof(PyTypeObject, tp_name), \ .tp_repr = offsetof(PyTypeObject, tp_repr), \ .tp_flags = offsetof(PyTypeObject, tp_flags), \ + .tp_basicsize = offsetof(PyTypeObject, tp_basicsize), \ + .tp_dictoffset = offsetof(PyTypeObject, tp_dictoffset), \ + }, \ + .heap_type_object = { \ + .size = sizeof(PyHeapTypeObject), \ + .ht_cached_keys = offsetof(PyHeapTypeObject, ht_cached_keys), \ }, \ .tuple_object = { \ .size = sizeof(PyTupleObject), \ @@ -347,10 +387,14 @@ typedef struct _Py_DebugOffsets { .state = offsetof(PyUnicodeObject, _base._base.state), \ .length = offsetof(PyUnicodeObject, _base._base.length), \ .asciiobject_size = sizeof(PyASCIIObject), \ + .compactunicodeobject_size = sizeof(PyCompactUnicodeObject), \ }, \ .gc = { \ .size = sizeof(struct _gc_runtime_state), \ .collecting = offsetof(struct _gc_runtime_state, collecting), \ + .frame = offsetof(struct _gc_runtime_state, frame), \ + .generation_stats_size = sizeof(struct gc_stats), \ + .generation_stats = offsetof(struct _gc_runtime_state, generation_stats), \ }, \ .gen_object = { \ .size = sizeof(PyGenObject), \ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_descrobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_descrobject.h index 3cec59a68..6143f8217 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_descrobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_descrobject.h @@ -22,6 +22,8 @@ typedef propertyobject _PyPropertyObject; extern PyTypeObject _PyMethodWrapper_Type; +extern void *_PyMember_GetOffset(PyObject *, PyMemberDef *); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_dict.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_dict.h index 77d99891b..37916fdf8 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_dict.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_dict.h @@ -23,6 +23,11 @@ PyAPI_FUNC(int) _PyDict_DelItemIf(PyObject *mp, PyObject *key, int (*predicate)(PyObject *value, void *arg), void *arg); +// Atomically replace an existing value if it is expected (by identity). +// Return 1 if replaced, 0 if absent or different, or -1 on error. +extern int _PyDict_ReplaceItemIf(PyObject *dict, PyObject *key, + PyObject *expected, PyObject *replacement); + // "KnownHash" variants // Export for '_asyncio' shared extension PyAPI_FUNC(int) _PyDict_SetItem_KnownHash(PyObject *mp, PyObject *key, @@ -31,23 +36,23 @@ PyAPI_FUNC(int) _PyDict_SetItem_KnownHash(PyObject *mp, PyObject *key, PyAPI_FUNC(int) _PyDict_DelItem_KnownHash(PyObject *mp, PyObject *key, Py_hash_t hash); -extern int _PyDict_DelItem_KnownHash_LockHeld(PyObject *mp, PyObject *key, +// Exported for external JIT support +PyAPI_FUNC(int) _PyDict_DelItem_KnownHash_LockHeld(PyObject *mp, PyObject *key, Py_hash_t hash); extern int _PyDict_Contains_KnownHash(PyObject *, PyObject *, Py_hash_t); -// "Id" variants -extern PyObject* _PyDict_GetItemIdWithError(PyObject *dp, - _Py_Identifier *key); -extern int _PyDict_ContainsId(PyObject *, _Py_Identifier *); -extern int _PyDict_SetItemId(PyObject *dp, _Py_Identifier *key, PyObject *item); -extern int _PyDict_DelItemId(PyObject *mp, _Py_Identifier *key); +extern void _PyDict_ClearKeysVersionLockHeld(PyObject *mp); extern int _PyDict_Next( PyObject *mp, Py_ssize_t *pos, PyObject **key, PyObject **value, Py_hash_t *hash); extern int _PyDict_HasOnlyStringKeys(PyObject *mp); +PyAPI_FUNC(PyObject *) _PyDict_Subscript(PyObject *self, PyObject *key); +PyAPI_FUNC(PyObject *) _PyDict_SubscriptKnownHash(PyObject *self, PyObject *key, Py_hash_t hash); +PyAPI_FUNC(int) _PyDict_StoreSubscript(PyObject *self, PyObject *key, PyObject *value); + // Export for '_ctypes' shared extension PyAPI_FUNC(Py_ssize_t) _PyDict_SizeOf(PyDictObject *); @@ -60,7 +65,7 @@ extern Py_ssize_t _PyDict_SizeOf_LockHeld(PyDictObject *); of a key wins, if override is 2, a KeyError with conflicting key as argument is raised. */ -PyAPI_FUNC(int) _PyDict_MergeEx(PyObject *mp, PyObject *other, int override); +PyAPI_FUNC(int) _PyDict_MergeUniq(PyObject *mp, PyObject *other, PyObject **dupkey); extern void _PyDict_DebugMallocStats(FILE *out); @@ -92,9 +97,15 @@ typedef struct { extern PyDictKeysObject *_PyDict_NewKeysForClass(PyHeapTypeObject *); extern PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *); +/* Implementations of the `|` and `|=` operators for dict, used by the + * specializing interpreter. */ +extern PyObject *_PyDict_Or(PyObject *self, PyObject *other); +extern PyObject *_PyDict_IOr(PyObject *self, PyObject *other); + /* Gets a version number unique to the current state of the keys of dict, if possible. - * Returns the version number, or zero if it was not possible to get a version number. */ -extern uint32_t _PyDictKeys_GetVersionForCurrentState( + * Returns the version number, or zero if it was not possible to get a version number. + * Exported for external JIT support */ +PyAPI_FUNC(uint32_t) _PyDictKeys_GetVersionForCurrentState( PyInterpreterState *interp, PyDictKeysObject *dictkeys); /* Gets a version number unique to the current state of the keys of dict, if possible. @@ -104,8 +115,9 @@ extern uint32_t _PyDictKeys_GetVersionForCurrentState( * * The caller must hold the per-object lock on dict. * - * Returns the version number, or zero if it was not possible to get a version number. */ -extern uint32_t _PyDict_GetKeysVersionForCurrentState( + * Returns the version number, or zero if it was not possible to get a version number. + * Exported for external JIT support */ +PyAPI_FUNC(uint32_t) _PyDict_GetKeysVersionForCurrentState( PyInterpreterState *interp, PyDictObject *dict); extern size_t _PyDict_KeysSize(PyDictKeysObject *keys); @@ -114,15 +126,18 @@ extern void _PyDictKeys_DecRef(PyDictKeysObject *keys); /* _Py_dict_lookup() returns index of entry which can be used like DK_ENTRIES(dk)[index]. * -1 when no entry found, -3 when compare raises error. + * Exported for external JIT support */ -extern Py_ssize_t _Py_dict_lookup(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject **value_addr); +PyAPI_FUNC(Py_ssize_t) _Py_dict_lookup(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject **value_addr); extern Py_ssize_t _Py_dict_lookup_threadsafe(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject **value_addr); extern Py_ssize_t _Py_dict_lookup_threadsafe_stackref(PyDictObject *mp, PyObject *key, Py_hash_t hash, _PyStackRef *value_addr); extern int _PyDict_GetMethodStackRef(PyDictObject *dict, PyObject *name, _PyStackRef *method); -extern Py_ssize_t _PyDict_LookupIndex(PyDictObject *, PyObject *); -extern Py_ssize_t _PyDictKeys_StringLookup(PyDictKeysObject* dictkeys, PyObject *key); +// Exported for external JIT support +PyAPI_FUNC(Py_ssize_t) _PyDict_LookupIndexAndValue(PyDictObject *, PyObject *, PyObject **); +PyAPI_FUNC(Py_ssize_t) _PyDict_LookupIndex(PyDictObject *, PyObject *); +PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookup(PyDictKeysObject* dictkeys, PyObject *key); /* Look up a string key in an all unicode dict keys, assign the keys object a version, and * store it in version. @@ -131,26 +146,30 @@ extern Py_ssize_t _PyDictKeys_StringLookup(PyDictKeysObject* dictkeys, PyObject * strings. * * Returns DKIX_EMPTY if the key is not present. + * + * Exported for external JIT support */ -extern Py_ssize_t _PyDictKeys_StringLookupAndVersion(PyDictKeysObject* dictkeys, PyObject *key, uint32_t *version); -extern Py_ssize_t _PyDictKeys_StringLookupSplit(PyDictKeysObject* dictkeys, PyObject *key); -PyAPI_FUNC(PyObject *)_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *); -PyAPI_FUNC(void) _PyDict_LoadGlobalStackRef(PyDictObject *, PyDictObject *, PyObject *, _PyStackRef *); +PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookupAndVersion(PyDictKeysObject* dictkeys, PyObject *key, uint32_t *version); +PyAPI_FUNC(Py_ssize_t) _PyDictKeys_StringLookupSplit(PyDictKeysObject* dictkeys, PyObject *key); +// Return the borrowed source dictionary, or NULL if absent or on error. +PyAPI_FUNC(PyObject *) _PyDict_LoadGlobalStackRef(PyDictObject *, PyDictObject *, PyObject *, _PyStackRef *); // Loads the __builtins__ object from the globals dict. Returns a new reference. extern PyObject *_PyDict_LoadBuiltinsFromGlobals(PyObject *globals); /* Consumes references to key and value */ PyAPI_FUNC(int) _PyDict_SetItem_Take2(PyDictObject *op, PyObject *key, PyObject *value); -extern int _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value); +PyAPI_FUNC(int) _PyDict_SetItem_Take2_KnownHash(PyDictObject *op, PyObject *key, PyObject *value, Py_hash_t hash); +// Exported for external JIT support +PyAPI_FUNC(int) _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value); // Export for '_asyncio' shared extension PyAPI_FUNC(int) _PyDict_SetItem_KnownHash_LockHeld(PyDictObject *mp, PyObject *key, PyObject *value, Py_hash_t hash); // Export for '_asyncio' shared extension PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash_LockHeld(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result); -extern int _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result); +PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result); extern int _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **result); -extern int _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr, PyObject *name, PyObject *value); +PyAPI_FUNC(int) _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr, PyObject *name, PyObject *value); extern int _PyDict_Pop_KnownHash( PyDictObject *dict, @@ -164,6 +183,9 @@ extern void _PyDict_Clear_LockHeld(PyObject *op); PyAPI_FUNC(void) _PyDict_EnsureSharedOnRead(PyDictObject *mp); #endif +// Export for '_elementtree' shared extension +PyAPI_FUNC(PyObject*) _PyDict_CopyAsDict(PyObject *op); + #define DKIX_EMPTY (-1) #define DKIX_DUMMY (-2) /* Used internally */ #define DKIX_ERROR (-3) @@ -270,6 +292,13 @@ static inline PyDictUnicodeEntry* DK_UNICODE_ENTRIES(PyDictKeysObject *dk) { #define DICT_UNIQUE_ID_SHIFT (32) #define DICT_UNIQUE_ID_MAX ((UINT64_C(1) << (64 - DICT_UNIQUE_ID_SHIFT)) - 1) +/* The first three dict watcher IDs are reserved for CPython, + * so we don't need to check that they haven't been used */ +#define BUILTINS_WATCHER_ID 0 +#define GLOBALS_WATCHER_ID 1 +#define MODULE_WATCHER_ID 2 +#define FIRST_AVAILABLE_WATCHER 3 + PyAPI_FUNC(void) _PyDict_SendEvent(int watcher_bits, @@ -279,8 +308,7 @@ _PyDict_SendEvent(int watcher_bits, PyObject *value); static inline void -_PyDict_NotifyEvent(PyInterpreterState *interp, - PyDict_WatchEvent event, +_PyDict_NotifyEvent(PyDict_WatchEvent event, PyDictObject *mp, PyObject *key, PyObject *value) @@ -318,6 +346,10 @@ _PyDictValues_AddToInsertionOrder(PyDictValues *values, Py_ssize_t ix) values->size = size+1; } +// Exported for external JIT support +PyAPI_FUNC(void) +_PyDict_InsertSplitValue(PyDictObject *mp, PyObject *key, PyObject *value, Py_ssize_t ix); + static inline size_t shared_keys_usable_size(PyDictKeysObject *keys) { @@ -368,7 +400,7 @@ _PyDict_UniqueId(PyDictObject *mp) static inline void _Py_INCREF_DICT(PyObject *op) { - assert(PyDict_Check(op)); + assert(PyAnyDict_Check(op)); Py_ssize_t id = _PyDict_UniqueId((PyDictObject *)op); _Py_THREAD_INCREF_OBJECT(op, id); } @@ -376,7 +408,7 @@ _Py_INCREF_DICT(PyObject *op) static inline void _Py_DECREF_DICT(PyObject *op) { - assert(PyDict_Check(op)); + assert(PyAnyDict_Check(op)); Py_ssize_t id = _PyDict_UniqueId((PyDictObject *)op); _Py_THREAD_DECREF_OBJECT(op, id); } @@ -406,6 +438,15 @@ _Py_DECREF_BUILTINS(PyObject *op) } #endif +/* frozendict */ +typedef struct { + PyDictObject ob_base; + Py_hash_t ma_hash; +} PyFrozenDictObject; + +#define _PyFrozenDictObject_CAST(op) \ + (assert(PyFrozenDict_Check(op)), _Py_CAST(PyFrozenDictObject*, (op))) + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_faulthandler.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_faulthandler.h index 78cd657e6..9ddd70d39 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_faulthandler.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_faulthandler.h @@ -42,6 +42,7 @@ struct faulthandler_user_signal { int chain; _Py_sighandler_t previous; PyInterpreterState *interp; + Py_ssize_t max_threads; }; #endif /* FAULTHANDLER_USER */ @@ -57,6 +58,7 @@ struct _faulthandler_runtime_state { void *exc_handler; #endif int c_stack; + Py_ssize_t max_threads; } fatal_error; struct { @@ -68,6 +70,7 @@ struct _faulthandler_runtime_state { int exit; char *header; size_t header_len; + Py_ssize_t max_threads; /* The main thread always holds this lock. It is only released when faulthandler_thread() is interrupted before this thread exits, or at Python exit. */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_floatobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_floatobject.h index 317f98418..62501cdaf 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_floatobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_floatobject.h @@ -12,7 +12,6 @@ extern "C" { /* runtime lifecycle */ -extern void _PyFloat_InitState(PyInterpreterState *); extern PyStatus _PyFloat_InitTypes(PyInterpreterState *); extern void _PyFloat_FiniType(PyInterpreterState *); @@ -42,6 +41,15 @@ extern double _Py_parse_inf_or_nan(const char *p, char **endptr); extern int _Py_convert_int_to_double(PyObject **v, double *dbl); +/* Should match endianness of the platform in most (all?) cases. */ + +#ifdef DOUBLE_IS_BIG_ENDIAN_IEEE754 +# define _PY_FLOAT_BIG_ENDIAN 1 +# define _PY_FLOAT_LITTLE_ENDIAN 0 +#else +# define _PY_FLOAT_BIG_ENDIAN 0 +# define _PY_FLOAT_LITTLE_ENDIAN 1 +#endif #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_flowgraph.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_flowgraph.h index 5043260d2..fc3ae6b36 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_flowgraph.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_flowgraph.h @@ -27,7 +27,7 @@ int _PyCfg_OptimizeCodeUnit(struct _PyCfgBuilder *g, PyObject *consts, PyObject struct _PyCfgBuilder* _PyCfg_FromInstructionSequence(_PyInstructionSequence *seq); int _PyCfg_ToInstructionSequence(struct _PyCfgBuilder *g, _PyInstructionSequence *seq); int _PyCfg_OptimizedCfgToInstructionSequence(struct _PyCfgBuilder *g, _PyCompile_CodeUnitMetadata *umd, - int code_flags, int *stackdepth, int *nlocalsplus, + int *stackdepth, int *nlocalsplus, _PyInstructionSequence *seq); PyCodeObject * diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_frame.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_frame.h index 8c410e9e2..3c9ab99c3 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_frame.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_frame.h @@ -38,23 +38,23 @@ struct _frame { PyObject *_f_frame_data[1]; }; -extern PyFrameObject* _PyFrame_New_NoTrack(PyCodeObject *code); +// Exported for external JIT support +PyAPI_FUNC(PyFrameObject *) _PyFrame_New_NoTrack(PyCodeObject *code); /* other API */ typedef enum _framestate { - FRAME_CREATED = -3, - FRAME_SUSPENDED = -2, - FRAME_SUSPENDED_YIELD_FROM = -1, - FRAME_EXECUTING = 0, - FRAME_COMPLETED = 1, - FRAME_CLEARED = 4 + FRAME_CREATED = 0, + FRAME_SUSPENDED = 1, + FRAME_SUSPENDED_YIELD_FROM = 2, + FRAME_SUSPENDED_YIELD_FROM_LOCKED = 3, + FRAME_EXECUTING = 4, + FRAME_CLEARED = 5 } PyFrameState; -#define FRAME_STATE_SUSPENDED(S) ((S) == FRAME_SUSPENDED || (S) == FRAME_SUSPENDED_YIELD_FROM) -#define FRAME_STATE_FINISHED(S) ((S) >= FRAME_COMPLETED) - +#define FRAME_STATE_SUSPENDED(S) ((S) >= FRAME_SUSPENDED && (S) <= FRAME_SUSPENDED_YIELD_FROM_LOCKED) +#define FRAME_STATE_FINISHED(S) ((S) == FRAME_CLEARED) #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist.h index f3c9a669a..3a41ec4b5 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist.h @@ -17,15 +17,16 @@ extern "C" { static inline struct _Py_freelists * _Py_freelists_GET(void) { - PyThreadState *tstate = _PyThreadState_GET(); #ifdef Py_DEBUG - _Py_EnsureTstateNotNULL(tstate); + _Py_AssertHoldsTstate(); #endif #ifdef Py_GIL_DISABLED + PyThreadState *tstate = _PyThreadState_GET(); return &((_PyThreadStateImpl*)tstate)->freelists; #else - return &tstate->interp->object_state.freelists; + PyInterpreterState *interp = _PyInterpreterState_GET(); + return &interp->object_state.freelists; #endif } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist_state.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist_state.h index 4828dfd94..46e2a82ea 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist_state.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_freelist_state.h @@ -16,6 +16,7 @@ extern "C" { # define Py_dicts_MAXFREELIST 80 # define Py_dictkeys_MAXFREELIST 80 # define Py_floats_MAXFREELIST 100 +# define Py_complexes_MAXFREELIST 100 # define Py_ints_MAXFREELIST 100 # define Py_slices_MAXFREELIST 1 # define Py_ranges_MAXFREELIST 6 @@ -26,6 +27,7 @@ extern "C" { # define Py_futureiters_MAXFREELIST 255 # define Py_object_stack_chunks_MAXFREELIST 4 # define Py_unicode_writers_MAXFREELIST 1 +# define Py_bytes_writers_MAXFREELIST 1 # define Py_pycfunctionobject_MAXFREELIST 16 # define Py_pycmethodobject_MAXFREELIST 16 # define Py_pymethodobjects_MAXFREELIST 20 @@ -43,6 +45,7 @@ struct _Py_freelist { struct _Py_freelists { struct _Py_freelist floats; + struct _Py_freelist complexes; struct _Py_freelist ints; struct _Py_freelist tuples[PyTuple_MAXSAVESIZE]; struct _Py_freelist lists; @@ -59,6 +62,7 @@ struct _Py_freelists { struct _Py_freelist futureiters; struct _Py_freelist object_stack_chunks; struct _Py_freelist unicode_writers; + struct _Py_freelist bytes_writers; struct _Py_freelist pycfunctionobject; struct _Py_freelist pycmethodobject; struct _Py_freelist pymethodobjects; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_function.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_function.h index 69b9ad548..2184f4095 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_function.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_function.h @@ -8,7 +8,7 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -extern PyObject* _PyFunction_Vectorcall( +PyAPI_FUNC(PyObject *) _PyFunction_Vectorcall( PyObject *func, PyObject *const *stack, size_t nargsf, @@ -27,10 +27,10 @@ _PyFunction_IsVersionValid(uint32_t version) return version >= FUNC_VERSION_FIRST_VALID; } -extern uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func); +// Exported for external JIT support +PyAPI_FUNC(uint32_t) _PyFunction_GetVersionForCurrentState(PyFunctionObject *func); PyAPI_FUNC(void) _PyFunction_SetVersion(PyFunctionObject *func, uint32_t version); void _PyFunction_ClearCodeByVersion(uint32_t version); -PyFunctionObject *_PyFunction_LookupByVersion(uint32_t version, PyObject **p_code); extern PyObject *_Py_set_function_type_params( PyThreadState* unused, PyObject *func, PyObject *type_params); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_gc.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_gc.h index bfe52f42f..84cbb56a9 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_gc.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_gc.h @@ -335,8 +335,9 @@ extern void _Py_RunGC(PyThreadState *tstate); union _PyStackRef; // GC visit callback for tracked interpreter frames -extern int _PyGC_VisitFrameStack(_PyInterpreterFrame *frame, visitproc visit, void *arg); -extern int _PyGC_VisitStackRef(union _PyStackRef *ref, visitproc visit, void *arg); +// GH-150766: exported for greenlet +PyAPI_FUNC(int) _PyGC_VisitFrameStack(_PyInterpreterFrame *frame, visitproc visit, void *arg); +PyAPI_FUNC(int) _PyGC_VisitStackRef(union _PyStackRef *ref, visitproc visit, void *arg); #ifdef Py_GIL_DISABLED extern void _PyGC_VisitObjectsWorldStopped(PyInterpreterState *interp, diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_genobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_genobject.h index c1fc3511f..c86ae242f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_genobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_genobject.h @@ -22,7 +22,7 @@ PyGenObject *_PyGen_GetGeneratorFromFrame(_PyInterpreterFrame *frame) } PyAPI_FUNC(PyObject *)_PyGen_yf(PyGenObject *); -extern void _PyGen_Finalize(PyObject *self); +extern int _PyGen_ClearFrame(PyGenObject *self); // Export for '_asyncio' shared extension PyAPI_FUNC(int) _PyGen_SetStopIterationValue(PyObject *); @@ -31,12 +31,17 @@ PyAPI_FUNC(int) _PyGen_SetStopIterationValue(PyObject *); PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **); PyAPI_FUNC(PyObject *)_PyCoro_GetAwaitableIter(PyObject *o); -extern PyObject *_PyAsyncGenValueWrapperNew(PyThreadState *state, PyObject *); +PyAPI_FUNC(PyObject *)_PyAsyncGenValueWrapperNew(PyThreadState *state, PyObject *); + +// Exported for external JIT support +PyAPI_FUNC(PyObject *) _PyCoro_ComputeOrigin(int origin_depth, _PyInterpreterFrame *current_frame); extern PyTypeObject _PyCoroWrapper_Type; extern PyTypeObject _PyAsyncGenWrappedValue_Type; extern PyTypeObject _PyAsyncGenAThrow_Type; +PyAPI_FUNC(PySendResult) _PyAsyncGenASend_Send(PyObject *iter, PyObject *arg, PyObject **result); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_objects_fini_generated.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_objects_fini_generated.h index 599785837..c53607949 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_objects_fini_generated.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_objects_fini_generated.h @@ -13,7 +13,7 @@ static inline void _PyStaticObject_CheckRefcnt(PyObject *obj) { if (!_Py_IsImmortal(obj)) { fprintf(stderr, "Immortal Object has less refcnt than expected.\n"); - _PyObject_Dump(obj); + PyObject_Dump(obj); } } #endif @@ -286,6 +286,774 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 254]); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 255]); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 256]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 257]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 258]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 259]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 260]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 261]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 262]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 263]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 264]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 265]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 266]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 267]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 268]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 269]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 270]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 271]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 272]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 273]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 274]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 275]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 276]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 277]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 278]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 279]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 280]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 281]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 282]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 283]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 284]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 285]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 286]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 287]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 288]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 289]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 290]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 291]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 292]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 293]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 294]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 295]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 296]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 297]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 298]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 299]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 300]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 301]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 302]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 303]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 304]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 305]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 306]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 307]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 308]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 309]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 310]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 311]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 312]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 313]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 314]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 315]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 316]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 317]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 318]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 319]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 320]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 321]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 322]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 323]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 324]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 325]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 326]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 327]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 328]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 329]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 330]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 331]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 332]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 333]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 334]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 335]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 336]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 337]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 338]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 339]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 340]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 341]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 342]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 343]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 344]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 345]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 346]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 347]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 348]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 349]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 350]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 351]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 352]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 353]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 354]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 355]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 356]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 357]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 358]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 359]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 360]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 361]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 362]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 363]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 364]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 365]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 366]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 367]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 368]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 369]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 370]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 371]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 372]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 373]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 374]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 375]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 376]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 377]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 378]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 379]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 380]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 381]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 382]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 383]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 384]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 385]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 386]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 387]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 388]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 389]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 390]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 391]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 392]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 393]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 394]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 395]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 396]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 397]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 398]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 399]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 400]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 401]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 402]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 403]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 404]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 405]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 406]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 407]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 408]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 409]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 410]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 411]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 412]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 413]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 414]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 415]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 416]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 417]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 418]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 419]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 420]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 421]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 422]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 423]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 424]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 425]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 426]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 427]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 428]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 429]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 430]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 431]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 432]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 433]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 434]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 435]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 436]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 437]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 438]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 439]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 440]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 441]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 442]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 443]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 444]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 445]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 446]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 447]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 448]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 449]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 450]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 451]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 452]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 453]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 454]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 455]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 456]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 457]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 458]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 459]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 460]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 461]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 462]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 463]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 464]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 465]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 466]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 467]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 468]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 469]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 470]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 471]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 472]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 473]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 474]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 475]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 476]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 477]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 478]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 479]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 480]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 481]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 482]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 483]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 484]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 485]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 486]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 487]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 488]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 489]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 490]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 491]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 492]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 493]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 494]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 495]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 496]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 497]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 498]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 499]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 500]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 501]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 502]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 503]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 504]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 505]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 506]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 507]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 508]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 509]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 510]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 511]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 512]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 513]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 514]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 515]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 516]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 517]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 518]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 519]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 520]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 521]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 522]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 523]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 524]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 525]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 526]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 527]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 528]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 529]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 530]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 531]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 532]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 533]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 534]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 535]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 536]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 537]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 538]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 539]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 540]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 541]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 542]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 543]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 544]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 545]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 546]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 547]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 548]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 549]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 550]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 551]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 552]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 553]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 554]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 555]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 556]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 557]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 558]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 559]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 560]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 561]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 562]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 563]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 564]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 565]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 566]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 567]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 568]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 569]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 570]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 571]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 572]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 573]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 574]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 575]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 576]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 577]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 578]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 579]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 580]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 581]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 582]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 583]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 584]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 585]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 586]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 587]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 588]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 589]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 590]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 591]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 592]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 593]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 594]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 595]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 596]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 597]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 598]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 599]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 600]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 601]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 602]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 603]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 604]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 605]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 606]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 607]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 608]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 609]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 610]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 611]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 612]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 613]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 614]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 615]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 616]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 617]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 618]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 619]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 620]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 621]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 622]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 623]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 624]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 625]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 626]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 627]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 628]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 629]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 630]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 631]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 632]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 633]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 634]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 635]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 636]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 637]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 638]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 639]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 640]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 641]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 642]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 643]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 644]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 645]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 646]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 647]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 648]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 649]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 650]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 651]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 652]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 653]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 654]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 655]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 656]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 657]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 658]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 659]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 660]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 661]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 662]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 663]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 664]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 665]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 666]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 667]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 668]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 669]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 670]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 671]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 672]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 673]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 674]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 675]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 676]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 677]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 678]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 679]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 680]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 681]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 682]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 683]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 684]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 685]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 686]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 687]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 688]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 689]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 690]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 691]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 692]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 693]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 694]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 695]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 696]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 697]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 698]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 699]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 700]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 701]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 702]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 703]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 704]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 705]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 706]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 707]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 708]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 709]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 710]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 711]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 712]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 713]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 714]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 715]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 716]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 717]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 718]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 719]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 720]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 721]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 722]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 723]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 724]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 725]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 726]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 727]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 728]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 729]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 730]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 731]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 732]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 733]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 734]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 735]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 736]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 737]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 738]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 739]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 740]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 741]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 742]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 743]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 744]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 745]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 746]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 747]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 748]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 749]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 750]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 751]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 752]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 753]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 754]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 755]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 756]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 757]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 758]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 759]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 760]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 761]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 762]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 763]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 764]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 765]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 766]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 767]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 768]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 769]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 770]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 771]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 772]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 773]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 774]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 775]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 776]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 777]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 778]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 779]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 780]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 781]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 782]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 783]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 784]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 785]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 786]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 787]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 788]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 789]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 790]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 791]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 792]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 793]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 794]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 795]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 796]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 797]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 798]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 799]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 800]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 801]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 802]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 803]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 804]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 805]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 806]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 807]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 808]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 809]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 810]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 811]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 812]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 813]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 814]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 815]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 816]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 817]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 818]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 819]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 820]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 821]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 822]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 823]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 824]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 825]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 826]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 827]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 828]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 829]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 830]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 831]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 832]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 833]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 834]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 835]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 836]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 837]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 838]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 839]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 840]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 841]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 842]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 843]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 844]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 845]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 846]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 847]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 848]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 849]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 850]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 851]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 852]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 853]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 854]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 855]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 856]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 857]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 858]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 859]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 860]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 861]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 862]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 863]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 864]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 865]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 866]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 867]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 868]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 869]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 870]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 871]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 872]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 873]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 874]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 875]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 876]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 877]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 878]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 879]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 880]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 881]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 882]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 883]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 884]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 885]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 886]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 887]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 888]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 889]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 890]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 891]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 892]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 893]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 894]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 895]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 896]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 897]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 898]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 899]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 900]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 901]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 902]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 903]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 904]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 905]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 906]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 907]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 908]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 909]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 910]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 911]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 912]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 913]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 914]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 915]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 916]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 917]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 918]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 919]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 920]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 921]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 922]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 923]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 924]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 925]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 926]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 927]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 928]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 929]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 930]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 931]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 932]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 933]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 934]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 935]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 936]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 937]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 938]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 939]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 940]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 941]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 942]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 943]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 944]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 945]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 946]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 947]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 948]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 949]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 950]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 951]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 952]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 953]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 954]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 955]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 956]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 957]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 958]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 959]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 960]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 961]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 962]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 963]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 964]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 965]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 966]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 967]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 968]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 969]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 970]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 971]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 972]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 973]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 974]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 975]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 976]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 977]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 978]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 979]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 980]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 981]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 982]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 983]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 984]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 985]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 986]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 987]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 988]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 989]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 990]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 991]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 992]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 993]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 994]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 995]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 996]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 997]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 998]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 999]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1000]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1001]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1002]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1003]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1004]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1005]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1006]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1007]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1008]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1009]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1010]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1011]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1012]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1013]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1014]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1015]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1016]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1017]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1018]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1019]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1020]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1021]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1022]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1023]); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(small_ints)[_PY_NSMALLNEGINTS + 1024]); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(bytes_characters)[0]); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(bytes_characters)[1]); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_SINGLETON(bytes_characters)[2]); @@ -558,16 +1326,32 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(dot_locals)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(empty)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(format)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(gc)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(generic_base)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(json_decoder)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(kwdefaults)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(list_err)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(native)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(str_replace_inf)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(type_params)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(utf_8)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_CLOSED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_CREATED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_RUNNING)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_SUSPENDED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CANCELLED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_CLOSED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_CREATED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_RUNNING)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(CORO_SUSPENDED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(Emax)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(Emin)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(FINISHED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(False)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_CLOSED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_CREATED)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_RUNNING)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(GEN_SUSPENDED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(JSONDecodeError)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(PENDING)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(Py_Repr)); @@ -662,6 +1446,8 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__iter__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__itruediv__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__ixor__)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__lazy_import__)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__lazy_modules__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__le__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__len__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__length_hint__)); @@ -758,6 +1544,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_filters)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_finalizing)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_find_and_load)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_find_and_load_lazy_submodule)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_fix_up_module)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_flags_)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(_get_sourcefile)); @@ -790,14 +1577,16 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(aclose)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(add)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(add_done_callback)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(adobe)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(after_in_child)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(after_in_parent)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(aggregate_class)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(alias)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(align)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(all)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(all_interpreters)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(all_threads)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(allow_code)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(alphabet)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(any)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(append)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(arg)); @@ -810,7 +1599,6 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ast)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(athrow)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(attribute)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(authorizer_callback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(autocommit)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(backtick)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(base)); @@ -820,6 +1608,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(bit_offset)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(bit_size)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(block)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(blocking)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(bound)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(buffer)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(buffer_callback)); @@ -837,6 +1626,8 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(c_exception)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(c_parameter_type)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(c_return)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(c_stack)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cache_frames)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cached_datetime_module)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cached_statements)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cadata)); @@ -844,13 +1635,19 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(call)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(call_exception_handler)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(call_soon)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(callable)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(callback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cancel)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(canonical)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(capath)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(capitals)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(capture_features)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(category)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cb_type)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(certfile)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(chain)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(check_same_thread)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(clamp)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(clear)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(close)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(closed)); @@ -876,9 +1673,12 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(co_varnames)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(code)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(col_offset)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(collector)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(command)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(comment_factory)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(compile_mode)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(compression)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(config)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(consts)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(context)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(contravariant)); @@ -889,11 +1689,14 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(coro)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(count)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(covariant)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ctx)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cwd)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(d_parameter_type)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(data)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(database)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(date)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(day)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(days)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(debug)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(decode)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(decoder)); @@ -906,7 +1709,6 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(deterministic)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(device)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dict)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dictcomp)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(difference_update)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(digest)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(digest_size)); @@ -921,9 +1723,11 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dont_inherit)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dst)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(dst_dir_fd)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(duration_sec)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(eager_start)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(effective_ids)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(element_factory)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(emptyerror)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(encode)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(encoding)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(end)); @@ -933,20 +1737,26 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(endpos)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(entrypoint)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(env)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(error_rate)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(errors)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(event)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(eventmask)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_tb)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_type)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_val)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_value)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(excepthook)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exception)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(existing_file_name)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exit)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exp)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(expression)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(extend)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(extra_tokens)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(facility)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(factory)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fallback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(false)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(family)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fanout)); @@ -969,19 +1779,22 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(flags)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(flush)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fold)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(foldspaces)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(follow_symlinks)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(format)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(format_spec)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(frame_buffer)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(free_threaded)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(from_param)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fromlist)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fromtimestamp)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fromutc)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fset)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fullerror)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(func)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(future)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(gc)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(generation)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(genexpr)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(get)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(get_debug)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(get_event_loop)); @@ -1002,13 +1815,17 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hi)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hook)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hour)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hours)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(id)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ident)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(identity_hint)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ignore)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ignorechars)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(imag)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(implieslink)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(importlib)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(in_fd)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(include_aliases)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(incoming)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(index)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(indexgroup)); @@ -1055,9 +1872,9 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kw)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kw1)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kw2)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kwargs)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(kwdefaults)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(label)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(lambda)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_exc)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_node)); @@ -1066,6 +1883,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(last_value)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(latin1)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(leaf_size)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(legacy)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(len)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(length)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(level)); @@ -1073,7 +1891,6 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(line)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(line_buffering)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(lineno)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(listcomp)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(little)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(lo)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(locale)); @@ -1082,12 +1899,15 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(loop)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(manual_reset)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mapping)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mask)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(match)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(max_length)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(max_threads)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxdigits)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxevents)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxlen)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxmem)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxsize)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxsplit)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(maxvalue)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(memLevel)); @@ -1097,19 +1917,22 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(metadata)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(method)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(microsecond)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(microseconds)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(milliseconds)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(minute)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(minutes)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(missed_samples)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mod)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mode)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(module)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(module_globals)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(modules)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(modulo)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(month)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mro)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(msg)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mutex)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(mycmp)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(n_arg)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(n_fields)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(n_sequence_fields)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(n_unnamed_fields)); @@ -1117,7 +1940,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(name_from)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(namespace_separator)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(namespaces)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(narg)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(native)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ndigits)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(nested)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(new_file_name)); @@ -1144,6 +1967,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(only_keys)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(oparg)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcode)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcodes)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(open)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opener)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(operation)); @@ -1151,11 +1975,14 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(options)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(order)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(origin)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(other)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(out_fd)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(outgoing)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(outpath)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(overlapped)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(owner)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pad)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(padded)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pages)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(parameter)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(parent)); @@ -1168,21 +1995,27 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(person)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pi_factory)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pid)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pointer_bits)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(policy)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pos)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pos1)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pos2)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(posix)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(prec)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(preserve_exc)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(print_file_and_line)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(priority)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress_handler)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress_callback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress_routine)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(proto)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(protocol)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ps1)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ps2)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(qid)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(qualname)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(query)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(queuetype)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(quotetabs)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(raw)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(read)); @@ -1194,24 +2027,34 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(readline)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(readonly)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(real)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(recursive)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reducer_override)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(registry)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(rel_tol)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(release)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reload)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(repeat)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(repl)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(replace)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(repr)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reqrefs)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(require_ready)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reserved)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reset)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(resetids)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(restrict)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(return)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reverse)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(reversed)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(rounding)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(salt)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sample_interval_us)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sample_rate)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sched_priority)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(scheduler)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(script)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(second)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(seconds)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(security_attributes)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(seek)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(seekable)); @@ -1223,18 +2066,21 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(server_hostname)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(server_side)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(session)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(setcomp)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(setpgroup)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(setsid)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(setsigdef)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(setsigmask)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(setstate)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(shape)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(shared)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(short)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(show_cmd)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(signed)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(signum)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(size)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sizehint)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(skip_file_prefixes)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(skip_non_matching_threads)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sleep)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sock)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sort)); @@ -1243,9 +2089,12 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(spam)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(src)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(src_dir_fd)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stack_frames)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stacklevel)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(start)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(start_time_us)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(statement)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stats)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(status)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stderr)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdin)); @@ -1263,6 +2112,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(symmetric_difference_update)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tabsize)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tag)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(take_bytes)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(target)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(target_is_directory)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(task)); @@ -1274,18 +2124,23 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(template)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(term)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(text)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(third)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(threading)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(throw)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(time)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timeout)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timer)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(times)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timespec)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timestamp)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timestamp_us)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timetuple)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(timeunit)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(top)); - _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(trace_callback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(traceback)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(trailers)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(translate)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(traps)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(true)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(truncate)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(twice)); @@ -1296,8 +2151,10 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tzinfo)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tzname)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(uid)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(unboundop)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(unlink)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(unraisablehook)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(updates)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(uri)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(usedforsecurity)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(value)); @@ -1311,9 +2168,11 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(wbits)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(week)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(weekday)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(weeks)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(which)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(who)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(withdata)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(wrapcol)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(writable)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(write)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(write_through)); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_strings.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_strings.h index bab5da70f..daa1f2ad2 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_strings.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_global_strings.h @@ -46,19 +46,35 @@ struct _Py_global_strings { STRUCT_FOR_STR(dot_locals, ".") STRUCT_FOR_STR(empty, "") STRUCT_FOR_STR(format, ".format") + STRUCT_FOR_STR(gc, "") STRUCT_FOR_STR(generic_base, ".generic_base") STRUCT_FOR_STR(json_decoder, "json.decoder") STRUCT_FOR_STR(kwdefaults, ".kwdefaults") STRUCT_FOR_STR(list_err, "list index out of range") + STRUCT_FOR_STR(native, "") STRUCT_FOR_STR(str_replace_inf, "1e309") STRUCT_FOR_STR(type_params, ".type_params") STRUCT_FOR_STR(utf_8, "utf-8") } literals; struct { + STRUCT_FOR_ID(AGEN_CLOSED) + STRUCT_FOR_ID(AGEN_CREATED) + STRUCT_FOR_ID(AGEN_RUNNING) + STRUCT_FOR_ID(AGEN_SUSPENDED) STRUCT_FOR_ID(CANCELLED) + STRUCT_FOR_ID(CORO_CLOSED) + STRUCT_FOR_ID(CORO_CREATED) + STRUCT_FOR_ID(CORO_RUNNING) + STRUCT_FOR_ID(CORO_SUSPENDED) + STRUCT_FOR_ID(Emax) + STRUCT_FOR_ID(Emin) STRUCT_FOR_ID(FINISHED) STRUCT_FOR_ID(False) + STRUCT_FOR_ID(GEN_CLOSED) + STRUCT_FOR_ID(GEN_CREATED) + STRUCT_FOR_ID(GEN_RUNNING) + STRUCT_FOR_ID(GEN_SUSPENDED) STRUCT_FOR_ID(JSONDecodeError) STRUCT_FOR_ID(PENDING) STRUCT_FOR_ID(Py_Repr) @@ -153,6 +169,8 @@ struct _Py_global_strings { STRUCT_FOR_ID(__iter__) STRUCT_FOR_ID(__itruediv__) STRUCT_FOR_ID(__ixor__) + STRUCT_FOR_ID(__lazy_import__) + STRUCT_FOR_ID(__lazy_modules__) STRUCT_FOR_ID(__le__) STRUCT_FOR_ID(__len__) STRUCT_FOR_ID(__length_hint__) @@ -249,6 +267,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(_filters) STRUCT_FOR_ID(_finalizing) STRUCT_FOR_ID(_find_and_load) + STRUCT_FOR_ID(_find_and_load_lazy_submodule) STRUCT_FOR_ID(_fix_up_module) STRUCT_FOR_ID(_flags_) STRUCT_FOR_ID(_get_sourcefile) @@ -281,14 +300,16 @@ struct _Py_global_strings { STRUCT_FOR_ID(aclose) STRUCT_FOR_ID(add) STRUCT_FOR_ID(add_done_callback) + STRUCT_FOR_ID(adobe) STRUCT_FOR_ID(after_in_child) STRUCT_FOR_ID(after_in_parent) - STRUCT_FOR_ID(aggregate_class) STRUCT_FOR_ID(alias) STRUCT_FOR_ID(align) STRUCT_FOR_ID(all) + STRUCT_FOR_ID(all_interpreters) STRUCT_FOR_ID(all_threads) STRUCT_FOR_ID(allow_code) + STRUCT_FOR_ID(alphabet) STRUCT_FOR_ID(any) STRUCT_FOR_ID(append) STRUCT_FOR_ID(arg) @@ -301,7 +322,6 @@ struct _Py_global_strings { STRUCT_FOR_ID(ast) STRUCT_FOR_ID(athrow) STRUCT_FOR_ID(attribute) - STRUCT_FOR_ID(authorizer_callback) STRUCT_FOR_ID(autocommit) STRUCT_FOR_ID(backtick) STRUCT_FOR_ID(base) @@ -311,6 +331,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(bit_offset) STRUCT_FOR_ID(bit_size) STRUCT_FOR_ID(block) + STRUCT_FOR_ID(blocking) STRUCT_FOR_ID(bound) STRUCT_FOR_ID(buffer) STRUCT_FOR_ID(buffer_callback) @@ -328,6 +349,8 @@ struct _Py_global_strings { STRUCT_FOR_ID(c_exception) STRUCT_FOR_ID(c_parameter_type) STRUCT_FOR_ID(c_return) + STRUCT_FOR_ID(c_stack) + STRUCT_FOR_ID(cache_frames) STRUCT_FOR_ID(cached_datetime_module) STRUCT_FOR_ID(cached_statements) STRUCT_FOR_ID(cadata) @@ -335,13 +358,19 @@ struct _Py_global_strings { STRUCT_FOR_ID(call) STRUCT_FOR_ID(call_exception_handler) STRUCT_FOR_ID(call_soon) + STRUCT_FOR_ID(callable) STRUCT_FOR_ID(callback) STRUCT_FOR_ID(cancel) + STRUCT_FOR_ID(canonical) STRUCT_FOR_ID(capath) + STRUCT_FOR_ID(capitals) + STRUCT_FOR_ID(capture_features) STRUCT_FOR_ID(category) STRUCT_FOR_ID(cb_type) STRUCT_FOR_ID(certfile) + STRUCT_FOR_ID(chain) STRUCT_FOR_ID(check_same_thread) + STRUCT_FOR_ID(clamp) STRUCT_FOR_ID(clear) STRUCT_FOR_ID(close) STRUCT_FOR_ID(closed) @@ -367,9 +396,12 @@ struct _Py_global_strings { STRUCT_FOR_ID(co_varnames) STRUCT_FOR_ID(code) STRUCT_FOR_ID(col_offset) + STRUCT_FOR_ID(collector) STRUCT_FOR_ID(command) STRUCT_FOR_ID(comment_factory) STRUCT_FOR_ID(compile_mode) + STRUCT_FOR_ID(compression) + STRUCT_FOR_ID(config) STRUCT_FOR_ID(consts) STRUCT_FOR_ID(context) STRUCT_FOR_ID(contravariant) @@ -380,11 +412,14 @@ struct _Py_global_strings { STRUCT_FOR_ID(coro) STRUCT_FOR_ID(count) STRUCT_FOR_ID(covariant) + STRUCT_FOR_ID(ctx) STRUCT_FOR_ID(cwd) STRUCT_FOR_ID(d_parameter_type) STRUCT_FOR_ID(data) STRUCT_FOR_ID(database) + STRUCT_FOR_ID(date) STRUCT_FOR_ID(day) + STRUCT_FOR_ID(days) STRUCT_FOR_ID(debug) STRUCT_FOR_ID(decode) STRUCT_FOR_ID(decoder) @@ -397,7 +432,6 @@ struct _Py_global_strings { STRUCT_FOR_ID(deterministic) STRUCT_FOR_ID(device) STRUCT_FOR_ID(dict) - STRUCT_FOR_ID(dictcomp) STRUCT_FOR_ID(difference_update) STRUCT_FOR_ID(digest) STRUCT_FOR_ID(digest_size) @@ -412,9 +446,11 @@ struct _Py_global_strings { STRUCT_FOR_ID(dont_inherit) STRUCT_FOR_ID(dst) STRUCT_FOR_ID(dst_dir_fd) + STRUCT_FOR_ID(duration_sec) STRUCT_FOR_ID(eager_start) STRUCT_FOR_ID(effective_ids) STRUCT_FOR_ID(element_factory) + STRUCT_FOR_ID(emptyerror) STRUCT_FOR_ID(encode) STRUCT_FOR_ID(encoding) STRUCT_FOR_ID(end) @@ -424,20 +460,26 @@ struct _Py_global_strings { STRUCT_FOR_ID(endpos) STRUCT_FOR_ID(entrypoint) STRUCT_FOR_ID(env) + STRUCT_FOR_ID(error_rate) STRUCT_FOR_ID(errors) STRUCT_FOR_ID(event) STRUCT_FOR_ID(eventmask) + STRUCT_FOR_ID(exc) + STRUCT_FOR_ID(exc_tb) STRUCT_FOR_ID(exc_type) + STRUCT_FOR_ID(exc_val) STRUCT_FOR_ID(exc_value) STRUCT_FOR_ID(excepthook) STRUCT_FOR_ID(exception) STRUCT_FOR_ID(existing_file_name) + STRUCT_FOR_ID(exit) STRUCT_FOR_ID(exp) STRUCT_FOR_ID(expression) STRUCT_FOR_ID(extend) STRUCT_FOR_ID(extra_tokens) STRUCT_FOR_ID(facility) STRUCT_FOR_ID(factory) + STRUCT_FOR_ID(fallback) STRUCT_FOR_ID(false) STRUCT_FOR_ID(family) STRUCT_FOR_ID(fanout) @@ -460,19 +502,22 @@ struct _Py_global_strings { STRUCT_FOR_ID(flags) STRUCT_FOR_ID(flush) STRUCT_FOR_ID(fold) + STRUCT_FOR_ID(foldspaces) STRUCT_FOR_ID(follow_symlinks) STRUCT_FOR_ID(format) STRUCT_FOR_ID(format_spec) STRUCT_FOR_ID(frame_buffer) + STRUCT_FOR_ID(free_threaded) STRUCT_FOR_ID(from_param) STRUCT_FOR_ID(fromlist) STRUCT_FOR_ID(fromtimestamp) STRUCT_FOR_ID(fromutc) STRUCT_FOR_ID(fset) + STRUCT_FOR_ID(fullerror) STRUCT_FOR_ID(func) STRUCT_FOR_ID(future) + STRUCT_FOR_ID(gc) STRUCT_FOR_ID(generation) - STRUCT_FOR_ID(genexpr) STRUCT_FOR_ID(get) STRUCT_FOR_ID(get_debug) STRUCT_FOR_ID(get_event_loop) @@ -493,13 +538,17 @@ struct _Py_global_strings { STRUCT_FOR_ID(hi) STRUCT_FOR_ID(hook) STRUCT_FOR_ID(hour) + STRUCT_FOR_ID(hours) STRUCT_FOR_ID(id) STRUCT_FOR_ID(ident) STRUCT_FOR_ID(identity_hint) STRUCT_FOR_ID(ignore) + STRUCT_FOR_ID(ignorechars) STRUCT_FOR_ID(imag) + STRUCT_FOR_ID(implieslink) STRUCT_FOR_ID(importlib) STRUCT_FOR_ID(in_fd) + STRUCT_FOR_ID(include_aliases) STRUCT_FOR_ID(incoming) STRUCT_FOR_ID(index) STRUCT_FOR_ID(indexgroup) @@ -546,9 +595,9 @@ struct _Py_global_strings { STRUCT_FOR_ID(kw) STRUCT_FOR_ID(kw1) STRUCT_FOR_ID(kw2) + STRUCT_FOR_ID(kwargs) STRUCT_FOR_ID(kwdefaults) STRUCT_FOR_ID(label) - STRUCT_FOR_ID(lambda) STRUCT_FOR_ID(last) STRUCT_FOR_ID(last_exc) STRUCT_FOR_ID(last_node) @@ -557,6 +606,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(last_value) STRUCT_FOR_ID(latin1) STRUCT_FOR_ID(leaf_size) + STRUCT_FOR_ID(legacy) STRUCT_FOR_ID(len) STRUCT_FOR_ID(length) STRUCT_FOR_ID(level) @@ -564,7 +614,6 @@ struct _Py_global_strings { STRUCT_FOR_ID(line) STRUCT_FOR_ID(line_buffering) STRUCT_FOR_ID(lineno) - STRUCT_FOR_ID(listcomp) STRUCT_FOR_ID(little) STRUCT_FOR_ID(lo) STRUCT_FOR_ID(locale) @@ -573,12 +622,15 @@ struct _Py_global_strings { STRUCT_FOR_ID(loop) STRUCT_FOR_ID(manual_reset) STRUCT_FOR_ID(mapping) + STRUCT_FOR_ID(mask) STRUCT_FOR_ID(match) STRUCT_FOR_ID(max_length) + STRUCT_FOR_ID(max_threads) STRUCT_FOR_ID(maxdigits) STRUCT_FOR_ID(maxevents) STRUCT_FOR_ID(maxlen) STRUCT_FOR_ID(maxmem) + STRUCT_FOR_ID(maxsize) STRUCT_FOR_ID(maxsplit) STRUCT_FOR_ID(maxvalue) STRUCT_FOR_ID(memLevel) @@ -588,19 +640,22 @@ struct _Py_global_strings { STRUCT_FOR_ID(metadata) STRUCT_FOR_ID(method) STRUCT_FOR_ID(microsecond) + STRUCT_FOR_ID(microseconds) STRUCT_FOR_ID(milliseconds) STRUCT_FOR_ID(minute) + STRUCT_FOR_ID(minutes) + STRUCT_FOR_ID(missed_samples) STRUCT_FOR_ID(mod) STRUCT_FOR_ID(mode) STRUCT_FOR_ID(module) STRUCT_FOR_ID(module_globals) STRUCT_FOR_ID(modules) + STRUCT_FOR_ID(modulo) STRUCT_FOR_ID(month) STRUCT_FOR_ID(mro) STRUCT_FOR_ID(msg) STRUCT_FOR_ID(mutex) STRUCT_FOR_ID(mycmp) - STRUCT_FOR_ID(n_arg) STRUCT_FOR_ID(n_fields) STRUCT_FOR_ID(n_sequence_fields) STRUCT_FOR_ID(n_unnamed_fields) @@ -608,7 +663,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(name_from) STRUCT_FOR_ID(namespace_separator) STRUCT_FOR_ID(namespaces) - STRUCT_FOR_ID(narg) + STRUCT_FOR_ID(native) STRUCT_FOR_ID(ndigits) STRUCT_FOR_ID(nested) STRUCT_FOR_ID(new_file_name) @@ -635,6 +690,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(only_keys) STRUCT_FOR_ID(oparg) STRUCT_FOR_ID(opcode) + STRUCT_FOR_ID(opcodes) STRUCT_FOR_ID(open) STRUCT_FOR_ID(opener) STRUCT_FOR_ID(operation) @@ -642,11 +698,14 @@ struct _Py_global_strings { STRUCT_FOR_ID(options) STRUCT_FOR_ID(order) STRUCT_FOR_ID(origin) + STRUCT_FOR_ID(other) STRUCT_FOR_ID(out_fd) STRUCT_FOR_ID(outgoing) STRUCT_FOR_ID(outpath) STRUCT_FOR_ID(overlapped) STRUCT_FOR_ID(owner) + STRUCT_FOR_ID(pad) + STRUCT_FOR_ID(padded) STRUCT_FOR_ID(pages) STRUCT_FOR_ID(parameter) STRUCT_FOR_ID(parent) @@ -659,21 +718,27 @@ struct _Py_global_strings { STRUCT_FOR_ID(person) STRUCT_FOR_ID(pi_factory) STRUCT_FOR_ID(pid) + STRUCT_FOR_ID(pointer_bits) STRUCT_FOR_ID(policy) STRUCT_FOR_ID(pos) STRUCT_FOR_ID(pos1) STRUCT_FOR_ID(pos2) STRUCT_FOR_ID(posix) + STRUCT_FOR_ID(prec) + STRUCT_FOR_ID(preserve_exc) STRUCT_FOR_ID(print_file_and_line) STRUCT_FOR_ID(priority) STRUCT_FOR_ID(progress) - STRUCT_FOR_ID(progress_handler) + STRUCT_FOR_ID(progress_callback) STRUCT_FOR_ID(progress_routine) STRUCT_FOR_ID(proto) STRUCT_FOR_ID(protocol) STRUCT_FOR_ID(ps1) STRUCT_FOR_ID(ps2) + STRUCT_FOR_ID(qid) + STRUCT_FOR_ID(qualname) STRUCT_FOR_ID(query) + STRUCT_FOR_ID(queuetype) STRUCT_FOR_ID(quotetabs) STRUCT_FOR_ID(raw) STRUCT_FOR_ID(read) @@ -685,24 +750,34 @@ struct _Py_global_strings { STRUCT_FOR_ID(readline) STRUCT_FOR_ID(readonly) STRUCT_FOR_ID(real) + STRUCT_FOR_ID(recursive) STRUCT_FOR_ID(reducer_override) STRUCT_FOR_ID(registry) STRUCT_FOR_ID(rel_tol) STRUCT_FOR_ID(release) STRUCT_FOR_ID(reload) + STRUCT_FOR_ID(repeat) STRUCT_FOR_ID(repl) STRUCT_FOR_ID(replace) + STRUCT_FOR_ID(repr) + STRUCT_FOR_ID(reqrefs) + STRUCT_FOR_ID(require_ready) STRUCT_FOR_ID(reserved) STRUCT_FOR_ID(reset) STRUCT_FOR_ID(resetids) + STRUCT_FOR_ID(restrict) STRUCT_FOR_ID(return) STRUCT_FOR_ID(reverse) STRUCT_FOR_ID(reversed) + STRUCT_FOR_ID(rounding) STRUCT_FOR_ID(salt) + STRUCT_FOR_ID(sample_interval_us) + STRUCT_FOR_ID(sample_rate) STRUCT_FOR_ID(sched_priority) STRUCT_FOR_ID(scheduler) STRUCT_FOR_ID(script) STRUCT_FOR_ID(second) + STRUCT_FOR_ID(seconds) STRUCT_FOR_ID(security_attributes) STRUCT_FOR_ID(seek) STRUCT_FOR_ID(seekable) @@ -714,18 +789,21 @@ struct _Py_global_strings { STRUCT_FOR_ID(server_hostname) STRUCT_FOR_ID(server_side) STRUCT_FOR_ID(session) - STRUCT_FOR_ID(setcomp) STRUCT_FOR_ID(setpgroup) STRUCT_FOR_ID(setsid) STRUCT_FOR_ID(setsigdef) STRUCT_FOR_ID(setsigmask) STRUCT_FOR_ID(setstate) STRUCT_FOR_ID(shape) + STRUCT_FOR_ID(shared) + STRUCT_FOR_ID(short) STRUCT_FOR_ID(show_cmd) STRUCT_FOR_ID(signed) + STRUCT_FOR_ID(signum) STRUCT_FOR_ID(size) STRUCT_FOR_ID(sizehint) STRUCT_FOR_ID(skip_file_prefixes) + STRUCT_FOR_ID(skip_non_matching_threads) STRUCT_FOR_ID(sleep) STRUCT_FOR_ID(sock) STRUCT_FOR_ID(sort) @@ -734,9 +812,12 @@ struct _Py_global_strings { STRUCT_FOR_ID(spam) STRUCT_FOR_ID(src) STRUCT_FOR_ID(src_dir_fd) + STRUCT_FOR_ID(stack_frames) STRUCT_FOR_ID(stacklevel) STRUCT_FOR_ID(start) + STRUCT_FOR_ID(start_time_us) STRUCT_FOR_ID(statement) + STRUCT_FOR_ID(stats) STRUCT_FOR_ID(status) STRUCT_FOR_ID(stderr) STRUCT_FOR_ID(stdin) @@ -754,6 +835,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(symmetric_difference_update) STRUCT_FOR_ID(tabsize) STRUCT_FOR_ID(tag) + STRUCT_FOR_ID(take_bytes) STRUCT_FOR_ID(target) STRUCT_FOR_ID(target_is_directory) STRUCT_FOR_ID(task) @@ -765,18 +847,23 @@ struct _Py_global_strings { STRUCT_FOR_ID(template) STRUCT_FOR_ID(term) STRUCT_FOR_ID(text) + STRUCT_FOR_ID(third) STRUCT_FOR_ID(threading) STRUCT_FOR_ID(throw) + STRUCT_FOR_ID(time) STRUCT_FOR_ID(timeout) STRUCT_FOR_ID(timer) STRUCT_FOR_ID(times) + STRUCT_FOR_ID(timespec) + STRUCT_FOR_ID(timestamp) + STRUCT_FOR_ID(timestamp_us) STRUCT_FOR_ID(timetuple) STRUCT_FOR_ID(timeunit) STRUCT_FOR_ID(top) - STRUCT_FOR_ID(trace_callback) STRUCT_FOR_ID(traceback) STRUCT_FOR_ID(trailers) STRUCT_FOR_ID(translate) + STRUCT_FOR_ID(traps) STRUCT_FOR_ID(true) STRUCT_FOR_ID(truncate) STRUCT_FOR_ID(twice) @@ -787,8 +874,10 @@ struct _Py_global_strings { STRUCT_FOR_ID(tzinfo) STRUCT_FOR_ID(tzname) STRUCT_FOR_ID(uid) + STRUCT_FOR_ID(unboundop) STRUCT_FOR_ID(unlink) STRUCT_FOR_ID(unraisablehook) + STRUCT_FOR_ID(updates) STRUCT_FOR_ID(uri) STRUCT_FOR_ID(usedforsecurity) STRUCT_FOR_ID(value) @@ -802,9 +891,11 @@ struct _Py_global_strings { STRUCT_FOR_ID(wbits) STRUCT_FOR_ID(week) STRUCT_FOR_ID(weekday) + STRUCT_FOR_ID(weeks) STRUCT_FOR_ID(which) STRUCT_FOR_ID(who) STRUCT_FOR_ID(withdata) + STRUCT_FOR_ID(wrapcol) STRUCT_FOR_ID(writable) STRUCT_FOR_ID(write) STRUCT_FOR_ID(write_through) diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_import.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_import.h index 13fbff4eb..f64b65e0f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_import.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_import.h @@ -17,7 +17,8 @@ extern int _PyImport_IsInitialized(PyInterpreterState *); // Export for 'pyexpat' shared extension PyAPI_FUNC(int) _PyImport_SetModule(PyObject *name, PyObject *module); -extern int _PyImport_SetModuleString(const char *name, PyObject* module); +// Export for 'math' shared extension +PyAPI_FUNC(int) _PyImport_SetModuleString(const char *name, PyObject* module); extern void _PyImport_AcquireLock(PyInterpreterState *interp); extern void _PyImport_ReleaseLock(PyInterpreterState *interp); @@ -31,6 +32,23 @@ extern int _PyImport_FixupBuiltin( PyObject *modules ); +extern PyObject * _PyImport_GetAbsName( + PyThreadState *tstate, PyObject *name, PyObject *globals, int level); +// Symbol is exported for the JIT on Windows builds. +PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate( + PyThreadState *tstate, PyObject *lazy_import); +// Bind a pending child on module before removing its registration. Return a +// new reference, NULL without an exception if absent, or NULL on error. +// Set recheck_dict if a concurrent lookup may have already bound the child. +extern PyObject * _PyImport_TryLoadLazySubmodule( + PyObject *module, PyObject *attr_name, int suppress, int *recheck_dict); +extern int _PyImport_ClearLazySubmodule( + PyThreadState *tstate, PyObject *name, int bind); +extern PyObject * _PyImport_LazyImportModuleLevelObject( + PyThreadState *tstate, PyObject *name, PyObject *builtins, + PyObject *globals, PyObject *locals, PyObject *fromlist, int level); + + #ifdef HAVE_DLOPEN # include // RTLD_NOW, RTLD_LAZY # if HAVE_DECL_RTLD_NOW @@ -68,11 +86,21 @@ extern void _PyImport_ClearModules(PyInterpreterState *interp); extern void _PyImport_ClearModulesByIndex(PyInterpreterState *interp); +extern PyObject * _PyImport_InitLazyModules( + PyInterpreterState *interp); +extern void _PyImport_ClearLazyModules(PyInterpreterState *interp); +extern int _PyImport_DiscardLazyModule( + PyInterpreterState *interp, PyObject *name); + extern int _PyImport_InitDefaultImportFunc(PyInterpreterState *interp); extern int _PyImport_IsDefaultImportFunc( PyInterpreterState *interp, PyObject *func); +extern int _PyImport_IsDefaultLazyImportFunc( + PyInterpreterState *interp, + PyObject *func); + extern PyObject * _PyImport_GetImportlibLoader( PyInterpreterState *interp, const char *loader_name); @@ -127,11 +155,18 @@ PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename); // state of the module argument: // - If module is NULL or a PyModuleObject with md_gil == Py_MOD_GIL_NOT_USED, // call _PyEval_DisableGIL(). -// - Otherwise, call _PyEval_EnableGILPermanent(). If the GIL was not already -// enabled permanently, issue a warning referencing the module's name. +// - Otherwise, call _PyImport_EnableGILAndWarn // // This function may raise an exception. extern int _PyImport_CheckGILForModule(PyObject *module, PyObject *module_name); +// Assuming that the GIL is enabled from a call to +// _PyEval_EnableGILTransient(), call _PyEval_EnableGILPermanent(). +// If the GIL was not already enabled permanently, issue a warning referencing +// the module's name. +// Leave a message in verbose mode. +// +// This function may raise an exception. +extern int _PyImport_EnableGILAndWarn(PyThreadState *, PyObject *module_name); #endif #ifdef __cplusplus diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_importdl.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_importdl.h index 525a16f6b..9ed87a544 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_importdl.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_importdl.h @@ -14,6 +14,34 @@ extern "C" { extern const char *_PyImport_DynLoadFiletab[]; +#ifdef HAVE_DYNAMIC_LOADING +/* ./configure sets HAVE_DYNAMIC_LOADING if dynamic loading of modules is + supported on this platform. configure will then compile and link in one + of the dynload_*.c files, as appropriate. We will call a function in + those modules to get a function pointer to the module's init function. + + The function should return: + - The function pointer on success + - NULL with exception set if the library cannot be loaded + - NULL *without* an extension set if the library could be loaded but the + function cannot be found in it. +*/ +#ifdef MS_WINDOWS +#include +typedef FARPROC dl_funcptr; +extern dl_funcptr _PyImport_FindSharedFuncptrWindows(const char *prefix, + const char *shortname, + PyObject *pathname, + FILE *fp); +#else +typedef void (*dl_funcptr)(void); +extern dl_funcptr _PyImport_FindSharedFuncptr(const char *prefix, + const char *shortname, + const char *pathname, FILE *fp); +#endif + +#endif /* HAVE_DYNAMIC_LOADING */ + typedef enum ext_module_kind { _Py_ext_module_kind_UNKNOWN = 0, @@ -28,6 +56,11 @@ typedef enum ext_module_origin { _Py_ext_module_origin_DYNAMIC = 3, } _Py_ext_module_origin; +struct hook_prefixes { + const char *const init_prefix; + const char *const export_prefix; +}; + /* Input for loading an extension module. */ struct _Py_ext_module_loader_info { PyObject *filename; @@ -40,7 +73,7 @@ struct _Py_ext_module_loader_info { * depending on if it's builtin or not. */ PyObject *path; _Py_ext_module_origin origin; - const char *hook_prefix; + const struct hook_prefixes *hook_prefixes; const char *newcontext; }; extern void _Py_ext_module_loader_info_clear( @@ -62,7 +95,9 @@ extern int _Py_ext_module_loader_info_init_from_spec( PyObject *spec); #endif -/* The result from running an extension module's init function. */ +/* The result from running an extension module's init function. + * Not used for modules defined via PyModExport (slots array). + */ struct _Py_ext_module_loader_result { PyModuleDef *def; PyObject *module; @@ -89,10 +124,11 @@ extern void _Py_ext_module_loader_result_apply_error( /* The module init function. */ typedef PyObject *(*PyModInitFunction)(void); +typedef PySlot *(*PyModExportFunction)(void); #ifdef HAVE_DYNAMIC_LOADING -extern PyModInitFunction _PyImport_GetModInitFunc( +extern int _PyImport_GetModuleExportHooks( struct _Py_ext_module_loader_info *info, - FILE *fp); + FILE *fp, PyModInitFunction *modinit, PyModExportFunction *modexport); #endif extern int _PyImport_RunModInitFunc( PyModInitFunction p0, @@ -104,10 +140,8 @@ extern int _PyImport_RunModInitFunc( #define MAXSUFFIXSIZE 12 #ifdef MS_WINDOWS -#include -typedef FARPROC dl_funcptr; -#ifdef _DEBUG +#ifdef Py_DEBUG # define PYD_DEBUG_SUFFIX "_d" #else # define PYD_DEBUG_SUFFIX "" @@ -128,8 +162,6 @@ typedef FARPROC dl_funcptr; #define PYD_TAGGED_SUFFIX PYD_DEBUG_SUFFIX "." PYD_SOABI ".pyd" #define PYD_UNTAGGED_SUFFIX PYD_DEBUG_SUFFIX ".pyd" -#else -typedef void (*dl_funcptr)(void); #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruction_sequence.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruction_sequence.h index b5c927735..6257b771f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruction_sequence.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruction_sequence.h @@ -73,6 +73,7 @@ int _PyInstructionSequence_SetAnnotationsCode(_PyInstructionSequence *seq, _PyInstructionSequence *annotations); int _PyInstructionSequence_AddNested(_PyInstructionSequence *seq, _PyInstructionSequence *nested); void PyInstructionSequence_Fini(_PyInstructionSequence *seq); +_PyInstruction _PyInstructionSequence_GetInstruction(_PyInstructionSequence *seq, int pos); extern PyTypeObject _PyInstructionSequence_Type; #define _PyInstructionSequence_Check(v) Py_IS_TYPE((v), &_PyInstructionSequence_Type) diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruments.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruments.h index 98684ff10..56b55e93a 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruments.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_instruments.h @@ -33,51 +33,52 @@ int _PyMonitoring_SetEvents(int tool_id, _PyMonitoringEventSet events); int _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEventSet events); int _PyMonitoring_GetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEventSet *events); -extern int + +// these are exported only for other re-generated interpreters to call +PyAPI_FUNC(int) _Py_call_instrumentation(PyThreadState *tstate, int event, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr); -extern int +PyAPI_FUNC(int) _Py_call_instrumentation_line(PyThreadState *tstate, _PyInterpreterFrame* frame, _Py_CODEUNIT *instr, _Py_CODEUNIT *prev); -extern int +PyAPI_FUNC(int) _Py_call_instrumentation_instruction( PyThreadState *tstate, _PyInterpreterFrame* frame, _Py_CODEUNIT *instr); -_Py_CODEUNIT * +PyAPI_FUNC(_Py_CODEUNIT *) _Py_call_instrumentation_jump( _Py_CODEUNIT *instr, PyThreadState *tstate, int event, _PyInterpreterFrame *frame, _Py_CODEUNIT *src, _Py_CODEUNIT *dest); -extern int +PyAPI_FUNC(int) _Py_call_instrumentation_arg(PyThreadState *tstate, int event, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr, PyObject *arg); -extern int +PyAPI_FUNC(int) _Py_call_instrumentation_2args(PyThreadState *tstate, int event, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr, PyObject *arg0, PyObject *arg1); -extern void +PyAPI_FUNC(void) _Py_call_instrumentation_exc2(PyThreadState *tstate, int event, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr, PyObject *arg0, PyObject *arg1); -extern PyObject _PyInstrumentation_MISSING; -extern PyObject _PyInstrumentation_DISABLE; +PyAPI_DATA(PyObject) _PyInstrumentation_MISSING; +PyAPI_DATA(PyObject) _PyInstrumentation_DISABLE; /* Total tool ids available */ #define PY_MONITORING_TOOL_IDS 8 -/* Count of all local monitoring events */ -#define _PY_MONITORING_LOCAL_EVENTS 11 -/* Count of all "real" monitoring events (not derived from other events) */ +/* Count of all "real" monitoring events (not derived from other events). + * "Other" events can now be turned on/disabled per code object. */ #define _PY_MONITORING_UNGROUPED_EVENTS 16 /* Count of all monitoring events */ #define _PY_MONITORING_EVENTS 19 /* Tables of which tools are active for each monitored event. */ typedef struct _Py_LocalMonitors { - uint8_t tools[_PY_MONITORING_LOCAL_EVENTS]; + uint8_t tools[_PY_MONITORING_UNGROUPED_EVENTS]; } _Py_LocalMonitors; typedef struct _Py_GlobalMonitors { @@ -120,6 +121,15 @@ typedef struct _PyCoMonitoringData { extern int _Py_Instrumentation_GetLine(PyCodeObject *code, _PyCoLineInstrumentationData *line_data, int index); +static inline uint8_t +_PyCode_GetOriginalOpcode(_PyCoLineInstrumentationData *line_data, int index) +{ + return line_data->data[index*line_data->bytes_per_entry]; +} + +// Exported for external JIT support +PyAPI_FUNC(uint8_t) _PyCode_Deinstrument(uint8_t opcode); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interp_structs.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interp_structs.h index b93d6d3c8..58a15eb87 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interp_structs.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interp_structs.h @@ -14,7 +14,7 @@ extern "C" { #include "pycore_structs.h" // PyHamtObject #include "pycore_tstate.h" // _PyThreadStateImpl #include "pycore_typedefs.h" // _PyRuntimeState - +#include "pycore_uop.h" // _PyBloomFilter #define CODE_MAX_WATCHERS 8 #define CONTEXT_MAX_WATCHERS 8 @@ -69,10 +69,11 @@ struct code_arena_st; struct trampoline_api_st { void* (*init_state)(void); void (*write_state)(void* state, const void *code_addr, - unsigned int code_size, PyCodeObject* code); + size_t code_size, PyCodeObject* code); int (*free_state)(void* state); void *state; Py_ssize_t code_padding; + Py_ssize_t code_alignment; }; #endif @@ -100,7 +101,6 @@ struct _ceval_runtime_state { // For example, we use a preallocated array // for the list of pending calls. struct _pending_calls pending_mainthread; - PyMutex unused_sys_trace_profile_mutex; // kept for ABI compatibility }; @@ -132,8 +132,6 @@ struct _atexit_runtime_state { //################### // interpreter atexit -typedef void (*atexit_datacallbackfunc)(void *); - typedef struct atexit_callback { atexit_datacallbackfunc func; void *data; @@ -162,10 +160,11 @@ struct atexit_state { typedef struct { // Tagged pointer to next object in the list. // 0 means the object is not tracked - uintptr_t _gc_next; + _Py_ALIGNED_DEF(_PyObject_MIN_ALIGNMENT, uintptr_t) _gc_next; // Tagged pointer to previous object in the list. // Lowest two bits are used for flags documented later. + // Those bits are made available by the struct's minimum alignment. uintptr_t _gc_prev; } PyGC_Head; @@ -178,34 +177,51 @@ struct gc_generation { generations */ }; -struct gc_collection_stats { - /* number of collected objects */ - Py_ssize_t collected; - /* total number of uncollectable objects (put into gc.garbage) */ - Py_ssize_t uncollectable; -}; - /* Running stats per generation */ struct gc_generation_stats { + PyTime_t ts_start; + PyTime_t ts_stop; /* total number of collections */ Py_ssize_t collections; /* total number of collected objects */ Py_ssize_t collected; /* total number of uncollectable objects (put into gc.garbage) */ Py_ssize_t uncollectable; + // Total number of objects considered for collection and traversed: + Py_ssize_t candidates; + // Total duration of the collection in seconds: + double duration; + /* heap_size on the start of the collection */ + Py_ssize_t heap_size; +}; + +#ifdef Py_GIL_DISABLED +#define GC_YOUNG_STATS_SIZE 1 +#define GC_OLD_STATS_SIZE 1 +#else +#define GC_YOUNG_STATS_SIZE 11 +#define GC_OLD_STATS_SIZE 3 +#endif +struct gc_young_stats_buffer { + struct gc_generation_stats items[GC_YOUNG_STATS_SIZE]; + int8_t index; +}; + +struct gc_old_stats_buffer { + struct gc_generation_stats items[GC_OLD_STATS_SIZE]; + int8_t index; }; /* If we change this, we need to change the default value in the - signature of gc.collect. */ + signature of gc.collect and change the size of PyStats.gc_stats */ #define NUM_GENERATIONS 3 -struct _gc_runtime_state { - /* List of objects that still need to be cleaned up, singly linked - * via their gc headers' gc_prev pointers. */ - PyObject *trash_delete_later; - /* Current call-stack depth of tp_dealloc calls. */ - int trash_delete_nesting; +struct gc_stats { + struct gc_young_stats_buffer young; + struct gc_old_stats_buffer old[2]; +}; +struct _gc_runtime_state { /* Is automatic collection enabled? */ int enabled; int debug; @@ -216,12 +232,14 @@ struct _gc_runtime_state { struct gc_generation young; struct gc_generation old[2]; #endif - /* a permanent generation which won't be collected */ struct gc_generation permanent_generation; - struct gc_generation_stats generation_stats[NUM_GENERATIONS]; + struct gc_stats *generation_stats; /* true if we are currently running the collector */ int collecting; + // The frame that started the current collection. It might be NULL even when + // collecting (if no Python frame is running): + _PyInterpreterFrame *frame; /* list of uncollectable objects */ PyObject *garbage; /* a list of callbacks to be invoked when collection is performed */ @@ -230,11 +248,6 @@ struct _gc_runtime_state { /* The number of live objects. */ Py_ssize_t heap_size; - /* dummy members to preserve other offsets */ - Py_ssize_t dummy1; /* was work_to_do */ - int dummy2; /* was visited_space */ - int dummy3; /* was phase */ - /* This is the number of objects that survived the last full collection. It approximates the number of long lived objects tracked by the GC. @@ -250,16 +263,6 @@ struct _gc_runtime_state { #ifdef Py_GIL_DISABLED /* True if gc.freeze() has been used. */ int freeze_active; - - /* Memory usage of the process (RSS + swap) after last GC. */ - Py_ssize_t last_mem; - - /* This accumulates the new object count whenever collection is deferred - due to the RSS increase condition not being meet. Reset on collection. */ - Py_ssize_t deferred_count; - - /* Mutex held for gc_should_collect_mem_usage(). */ - PyMutex mutex; #else PyGC_Head *generation0; #endif @@ -342,6 +345,21 @@ struct _import_state { int dlopenflags; #endif PyObject *import_func; + PyObject *lazy_import_func; + int lazy_imports_mode; + PyObject *lazy_imports_filter; + // The set stored in sys.lazy_modules if values that have been + // lazily imported. This value is only for debugging/introspection + // purposes and is not used by the runtime. + PyObject *lazy_modules; + // Package names map to pending children: declarations for plain imports, + // or None for from-import names that may be ordinary attributes. + PyObject *lazy_pending_submodules; + // Avoid pending-child work for ordinary cached imports. + int has_lazy_submodules; +#ifdef Py_GIL_DISABLED + PyMutex lazy_mutex; +#endif /* The global import lock. */ _PyRecursiveMutex lock; /* diagnostic info in PyImport_ImportModuleLevelObject() */ @@ -416,6 +434,28 @@ typedef struct _rare_events { uint8_t func_modification; } _rare_events; +// Optimization configuration for the interpreter. +// This groups all thresholds and optimization flags for both JIT and interpreter. +typedef struct _PyOptimizationConfig { + // Interpreter optimization thresholds + uint16_t jump_backward_initial_value; + uint16_t jump_backward_initial_backoff; + + uint16_t resume_initial_value; + uint16_t resume_initial_backoff; + + // JIT optimization thresholds + uint16_t side_exit_initial_value; + uint16_t side_exit_initial_backoff; + + // Trace fitness thresholds + uint16_t fitness_initial; + + // Optimization flags + bool specialization_enabled; + bool uops_optimize_enabled; +} _PyOptimizationConfig; + struct Bigint { struct Bigint *next; @@ -489,8 +529,13 @@ struct _py_func_state { /****** type state *********/ /* For now we hard-code this to a value for which we are confident - all the static builtin types will fit (for all builds). */ -#define _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES 200 + all the static builtin types will fit (for all builds). + If you add a new static type to the standard library, you may have to + update one of these numbers. + */ +#define _Py_NUM_MANAGED_PREINITIALIZED_TYPES 120 +#define _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES \ + (_Py_NUM_MANAGED_PREINITIALIZED_TYPES + 83) #define _Py_MAX_MANAGED_STATIC_EXT_TYPES 10 #define _Py_MAX_MANAGED_STATIC_TYPES \ (_Py_MAX_MANAGED_STATIC_BUILTIN_TYPES + _Py_MAX_MANAGED_STATIC_EXT_TYPES) @@ -701,8 +746,6 @@ struct _Py_interp_cached_objects { /* object.__reduce__ */ PyObject *objreduce; - PyObject *type_slots_pname; - pytype_slotdef *type_slots_ptrs[MAX_EQUIV]; /* TypeVar and related types */ PyTypeObject *generic_type; @@ -712,6 +755,13 @@ struct _Py_interp_cached_objects { PyTypeObject *paramspecargs_type; PyTypeObject *paramspeckwargs_type; PyTypeObject *constevaluator_type; + + /* Descriptors for __dict__ and __weakref__ */ +#ifdef Py_GIL_DISABLED + PyMutex descriptor_mutex; +#endif + PyObject *dict_descriptor; + PyObject *weakref_descriptor; }; struct _Py_interp_static_objects { @@ -779,6 +829,9 @@ struct _Py_unique_id_pool { #endif +typedef _Py_CODEUNIT *(*_PyJitEntryFuncPtr)(struct _PyExecutorObject *exec, _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate); + +#define _PyInterpreterGuard_GUARDS_NOT_ALLOWED UINTPTR_MAX /* PyInterpreterState holds the global state for one of the runtime's interpreters. Typically the initial (main) interpreter is the only one. @@ -792,9 +845,6 @@ struct _is { * and should be placed at the beginning. */ struct _ceval_state ceval; - // unused, kept for ABI compatibility - void *_malloced; - PyInterpreterState *next; int64_t id; @@ -886,6 +936,7 @@ struct _is { PyObject *builtins_copy; // Initialized to _PyEval_EvalFrameDefault(). _PyFrameEvalFunction eval_frame; + int eval_frame_allow_specialization; PyFunction_WatchCallback func_watchers[FUNC_MAX_WATCHERS]; // One bit is set for each non-NULL entry in func_watchers @@ -959,10 +1010,18 @@ struct _is { struct callable_cache callable_cache; PyObject *common_consts[NUM_COMMON_CONSTANTS]; bool jit; - struct _PyExecutorObject *executor_list_head; + bool compiling; + + // Optimization configuration (thresholds and flags for JIT and interpreter) + _PyOptimizationConfig opt_config; + _PyBloomFilter *executor_blooms; // Contiguous bloom filter array + struct _PyExecutorObject **executor_ptrs; // Corresponding executor pointer array + size_t executor_count; // Number of valid executors + size_t executor_capacity; // Array capacity struct _PyExecutorObject *executor_deletion_list_head; - int executor_deletion_list_remaining_capacity; - size_t trace_run_counter; + struct _PyExecutorObject *cold_executor; + struct _PyExecutorObject *cold_dynamic_executor; + size_t executor_creation_counter; _rare_events rare_events; PyDict_WatchCallback builtins_dict_watcher; @@ -988,6 +1047,23 @@ struct _is { # endif #endif +#ifdef Py_STATS + // true if recording of pystats is on, this is used when new threads + // are created to decide if recording should be on for them + int pystats_enabled; + // allocated when (and if) stats are first enabled + PyStats *pystats_struct; +#ifdef Py_GIL_DISABLED + // held when pystats related interpreter state is being updated + PyMutex pystats_mutex; +#endif +#endif + + // The number of remaining finalization guards. + // If this is _PyInterpreterGuard_GUARDS_NOT_ALLOWED, then finalization + // guards can no longer be created. + uintptr_t finalization_guards; + /* the initial PyInterpreterState.threads.head */ _PyThreadStateImpl _initial_thread; // _initial_thread should be the last field of PyInterpreterState. diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe.h index b5b729f14..926eda85f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe.h @@ -27,7 +27,7 @@ static inline PyCodeObject *_PyFrame_GetCode(_PyInterpreterFrame *f) { // Similar to _PyFrame_GetCode(), but return NULL if the frame is invalid or // freed. Used by dump_frame() in Python/traceback.c. The function uses // heuristics to detect freed memory, it's not 100% reliable. -static inline PyCodeObject* +static inline PyCodeObject* _Py_NO_SANITIZE_THREAD _PyFrame_SafeGetCode(_PyInterpreterFrame *f) { // globals and builtins may be NULL on a legit frame, but it's unlikely. @@ -70,7 +70,7 @@ _PyFrame_GetBytecode(_PyInterpreterFrame *f) // Similar to PyUnstable_InterpreterFrame_GetLasti(), but return NULL if the // frame is invalid or freed. Used by dump_frame() in Python/traceback.c. The // function uses heuristics to detect freed memory, it's not 100% reliable. -static inline int +static inline int _Py_NO_SANITIZE_THREAD _PyFrame_SafeGetLasti(struct _PyInterpreterFrame *f) { // Code based on _PyFrame_GetBytecode() but replace _PyFrame_GetCode() @@ -102,14 +102,14 @@ static inline _PyStackRef *_PyFrame_Stackbase(_PyInterpreterFrame *f) { return (f->localsplus + _PyFrame_GetCode(f)->co_nlocalsplus); } -static inline _PyStackRef _PyFrame_StackPeek(_PyInterpreterFrame *f) { - assert(f->stackpointer > f->localsplus + _PyFrame_GetCode(f)->co_nlocalsplus); - assert(!PyStackRef_IsNull(f->stackpointer[-1])); - return f->stackpointer[-1]; +static inline _PyStackRef _PyFrame_StackPeek(_PyInterpreterFrame *f, int depth) { + assert(f->stackpointer > _PyFrame_Stackbase(f)); + assert(!PyStackRef_IsNull(f->stackpointer[-depth])); + return f->stackpointer[-depth]; } static inline _PyStackRef _PyFrame_StackPop(_PyInterpreterFrame *f) { - assert(f->stackpointer > f->localsplus + _PyFrame_GetCode(f)->co_nlocalsplus); + assert(f->stackpointer > _PyFrame_Stackbase(f)); f->stackpointer--; return *f->stackpointer; } @@ -282,9 +282,26 @@ _PyThreadState_GetFrame(PyThreadState *tstate) return _PyFrame_GetFirstComplete(tstate->current_frame); } +// Update last_profiled_frame for remote profiler frame caching. +// Only update if we're removing the exact frame that was last profiled. +// This avoids corrupting the cache when transient frames (called and returned +// between profiler samples) update last_profiled_frame to addresses the +// profiler never saw. +// The sequence distinguishes this anchor from a later frame that reuses the +// same _PyInterpreterFrame address. +#define _PyThreadState_UpdateLastProfiledFrame(tstate, frame, previous) \ + do { \ + PyThreadState *tstate_ = (tstate); \ + _PyInterpreterFrame *frame_ = (frame); \ + if (tstate_->last_profiled_frame == frame_) { \ + tstate_->last_profiled_frame = (previous); \ + tstate_->last_profiled_frame_seq++; \ + } \ + } while (0) + /* For use by _PyFrame_GetFrameObject Do not call directly. */ -PyFrameObject * +PyAPI_FUNC(PyFrameObject *) _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame); /* Gets the PyFrameObject for this frame, lazily @@ -302,7 +319,8 @@ _PyFrame_GetFrameObject(_PyInterpreterFrame *frame) return _PyFrame_MakeAndSetFrameObject(frame); } -void +// Exported for external JIT support +PyAPI_FUNC(void) _PyFrame_ClearLocals(_PyInterpreterFrame *frame); /* Clears all references in the frame. @@ -313,8 +331,10 @@ _PyFrame_ClearLocals(_PyInterpreterFrame *frame); * in the frame. * take should be set to 1 for heap allocated * frames like the ones in generators and coroutines. + * + * Exported for external JIT support */ -void + PyAPI_FUNC(void) _PyFrame_ClearExceptCode(_PyInterpreterFrame * frame); int @@ -338,7 +358,8 @@ _PyThreadState_HasStackSpace(PyThreadState *tstate, int size) size < tstate->datastack_limit - tstate->datastack_top; } -extern _PyInterpreterFrame * +// Exported for external JIT support +PyAPI_FUNC(_PyInterpreterFrame *) _PyThreadState_PushFrame(PyThreadState *tstate, size_t size); PyAPI_FUNC(void) _PyThreadState_PopFrame(PyThreadState *tstate, _PyInterpreterFrame *frame); @@ -400,6 +421,10 @@ _PyEvalFramePushAndInit(PyThreadState *tstate, _PyStackRef func, size_t argcount, PyObject *kwnames, _PyInterpreterFrame *previous); +PyAPI_FUNC(_PyInterpreterFrame *) +_PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, + PyObject *locals, Py_ssize_t nargs, PyObject *callargs, PyObject *kwargs, _PyInterpreterFrame *previous); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe_structs.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe_structs.h index 835b8e581..38510685f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe_structs.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_interpframe_structs.h @@ -24,7 +24,6 @@ enum _frameowner { FRAME_OWNED_BY_GENERATOR = 1, FRAME_OWNED_BY_FRAME_OBJECT = 2, FRAME_OWNED_BY_INTERPRETER = 3, - FRAME_OWNED_BY_CSTACK = 4, }; struct _PyInterpreterFrame { diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit.h index 8a88cbf60..2f97cc26e 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit.h @@ -13,12 +13,24 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +/* To be able to reason about code layout and branches, keep code size below 1 MB */ +#define PY_MAX_JIT_CODE_SIZE ((1 << 20)-1) + #ifdef _Py_JIT -typedef _Py_CODEUNIT *(*jit_func)(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate); +typedef _Py_CODEUNIT *(*jit_func)( + _PyExecutorObject *executor, _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate, + _PyStackRef _tos_cache0, _PyStackRef _tos_cache1, _PyStackRef _tos_cache2 +); + +_Py_CODEUNIT *_PyJIT_Entry( + _PyExecutorObject *executor, _PyInterpreterFrame *frame, + _PyStackRef *stack_pointer, PyThreadState *tstate +); int _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction *trace, size_t length); void _PyJIT_Free(_PyExecutorObject *executor); +PyAPI_FUNC(int) _PyJIT_AddressInJitCode(PyInterpreterState *interp, uintptr_t addr); #endif // _Py_JIT diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit_publish.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit_publish.h new file mode 100644 index 000000000..7ee77f5c9 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit_publish.h @@ -0,0 +1,31 @@ +#ifndef Py_INTERNAL_JIT_PUBLISH_H +#define Py_INTERNAL_JIT_PUBLISH_H + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include + +typedef struct _PyJitCodeRegistration _PyJitCodeRegistration; + +#ifdef _Py_JIT + +/* Publish JIT code to optional tooling backends. + * + * The return value is a backend-specific deregistration handle, not a + * success/failure indicator. NULL means there is nothing to unregister later: + * perf does not need a handle, and GDB/GNU backtrace registration failures + * are intentionally non-fatal because tooling support must not make JIT + * compilation fail. + */ +_PyJitCodeRegistration *_PyJit_RegisterCode(const void *code_addr, + size_t code_size, + const char *entry, + const char *filename); + +void _PyJit_UnregisterCode(_PyJitCodeRegistration *registration); + +#endif // _Py_JIT + +#endif // Py_INTERNAL_JIT_PUBLISH_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit_unwind.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit_unwind.h new file mode 100644 index 000000000..7099b8881 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_jit_unwind.h @@ -0,0 +1,81 @@ +#ifndef Py_INTERNAL_JIT_UNWIND_H +#define Py_INTERNAL_JIT_UNWIND_H + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include +#include + +#if defined(_Py_JIT) && defined(__linux__) && defined(__ELF__) +# define PY_HAVE_JIT_GDB_UNWIND +# if defined(HAVE_EXECINFO_H) && defined(HAVE_BACKTRACE) && \ + defined(_Py_HAVE_LIBGCC_EH_FRAME_REGISTRATION) +# define PY_HAVE_JIT_GNU_BACKTRACE_UNWIND +# endif +#endif + +#if defined(PY_HAVE_PERF_TRAMPOLINE) \ + || defined(PY_HAVE_JIT_GDB_UNWIND) \ + || defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND) + +#if defined(PY_HAVE_JIT_GDB_UNWIND) +extern PyMutex _Py_jit_debug_mutex; +#endif + +/* DWARF exception-handling pointer encodings shared by JIT unwind users. */ +enum { + DWRF_EH_PE_absptr = 0x00, + DWRF_EH_PE_omit = 0xff, + + /* Data type encodings */ + DWRF_EH_PE_uleb128 = 0x01, + DWRF_EH_PE_udata2 = 0x02, + DWRF_EH_PE_udata4 = 0x03, + DWRF_EH_PE_udata8 = 0x04, + DWRF_EH_PE_sleb128 = 0x09, + DWRF_EH_PE_sdata2 = 0x0a, + DWRF_EH_PE_sdata4 = 0x0b, + DWRF_EH_PE_sdata8 = 0x0c, + DWRF_EH_PE_signed = 0x08, + + /* Reference type encodings */ + DWRF_EH_PE_pcrel = 0x10, + DWRF_EH_PE_textrel = 0x20, + DWRF_EH_PE_datarel = 0x30, + DWRF_EH_PE_funcrel = 0x40, + DWRF_EH_PE_aligned = 0x50, + DWRF_EH_PE_indirect = 0x80 +}; + +/* Return the size of the generated .eh_frame data for the given encoding. */ +size_t _PyJitUnwind_EhFrameSize(int absolute_addr); + +/* + * Build DWARF .eh_frame data for JIT code; returns size written or 0 on error. + * absolute_addr selects the FDE address encoding: + * - 0: PC-relative offsets (perf jitdump synthesized DSO). + * - nonzero: absolute addresses (GDB JIT in-memory ELF). + */ +size_t _PyJitUnwind_BuildEhFrame(uint8_t *buffer, size_t buffer_size, + const void *code_addr, size_t code_size, + int absolute_addr); + +void *_PyJitUnwind_GdbRegisterCode(const void *code_addr, + size_t code_size, + const char *entry, + const char *filename); + +void _PyJitUnwind_GdbUnregisterCode(void *handle); + +#if defined(PY_HAVE_JIT_GNU_BACKTRACE_UNWIND) +void *_PyJitUnwind_GnuBacktraceRegisterCode(const void *code_addr, + size_t code_size); + +void _PyJitUnwind_GnuBacktraceUnregisterCode(void *handle); +#endif + +#endif // JIT unwind support + +#endif // Py_INTERNAL_JIT_UNWIND_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_lazyimportobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_lazyimportobject.h new file mode 100644 index 000000000..2da037b0b --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_lazyimportobject.h @@ -0,0 +1,37 @@ +// Lazy object interface. + +#ifndef Py_INTERNAL_LAZYIMPORTOBJECT_H +#define Py_INTERNAL_LAZYIMPORTOBJECT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +PyAPI_DATA(PyTypeObject) PyLazyImport_Type; +#define PyLazyImport_CheckExact(op) Py_IS_TYPE((op), &PyLazyImport_Type) + +PyAPI_FUNC(PyObject *) _PyLazyImport_New( + struct _PyInterpreterFrame *frame, PyObject *builtins, + PyObject *name, PyObject *fromlist); + +extern PyObject *_PyLazyImport_LoadChild( + PyThreadState *tstate, PyObject *declaration, PyObject *name); +extern int _PyLazyImport_IsActive(PyObject *declaration); + +extern int _PyLazyImport_IsResolving(PyThreadState *tstate, PyObject *op); + +// Resolve a placeholder and replace its binding if it is unchanged or holds +// the child module published by the normal importer during resolution. +// namespace is the source captured during lookup, before resolution runs. +PyAPI_FUNC(PyObject *) _PyLazyImport_Reify( + PyThreadState *tstate, PyObject *placeholder, + PyObject *name, PyObject *ns); + +#ifdef __cplusplus +} +#endif +#endif // !Py_INTERNAL_LAZYIMPORTOBJECT_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_list.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_list.h index ffbcebdb7..df0d00f75 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_list.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_list.h @@ -14,15 +14,17 @@ extern "C" { PyAPI_FUNC(PyObject*) _PyList_Extend(PyListObject *, PyObject *); PyAPI_FUNC(PyObject) *_PyList_SliceSubscript(PyObject*, PyObject*); +PyAPI_FUNC(PyObject *) _PyList_BinarySlice(PyObject *, PyObject *, PyObject *); +PyAPI_FUNC(PyObject *) _PyList_Concat(PyObject *, PyObject *); extern void _PyList_DebugMallocStats(FILE *out); // _PyList_GetItemRef should be used only when the object is known as a list // because it doesn't raise TypeError when the object is not a list, whereas PyList_GetItemRef does. -extern PyObject* _PyList_GetItemRef(PyListObject *, Py_ssize_t i); +PyAPI_FUNC(PyObject *) _PyList_GetItemRef(PyListObject *, Py_ssize_t i); #ifdef Py_GIL_DISABLED // Returns -1 in case of races with other threads. -extern int _PyList_GetItemRefNoLock(PyListObject *, Py_ssize_t, _PyStackRef *); +PyAPI_FUNC(int) _PyList_GetItemRefNoLock(PyListObject *, Py_ssize_t, _PyStackRef *); #endif #define _PyList_ITEMS(op) _Py_RVALUE(_PyList_CAST(op)->ob_item) diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_lock.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_lock.h index 9b071573a..e31d8b4e5 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_lock.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_lock.h @@ -13,7 +13,7 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -//_Py_UNLOCKED is defined as 0 and _Py_LOCKED as 1 in Include/cpython/lock.h +//_Py_UNLOCKED is defined as 0 and _Py_LOCKED as 1 in Include/cpython/pylock.h #define _Py_HAS_PARKED 2 #define _Py_ONCE_INITIALIZED 4 @@ -41,6 +41,14 @@ typedef enum _PyLockFlags { // Handle signals if interrupted while waiting on the lock. _PY_LOCK_HANDLE_SIGNALS = 2, + + // Fail if interrupted by a signal while waiting on the lock. + _PY_FAIL_IF_INTERRUPTED = 4, + + // Locking & unlocking this lock requires attached thread state. + // If locking returns PY_LOCK_FAILURE, a Python exception *may* be raised. + // (Intended for use with _PY_LOCK_HANDLE_SIGNALS and _PY_LOCK_DETACH.) + _PY_LOCK_PYTHONLOCK = 8, } _PyLockFlags; // Lock a mutex with an optional timeout and additional options. See @@ -62,6 +70,9 @@ PyMutex_LockFlags(PyMutex *m, _PyLockFlags flags) // error messages) otherwise returns 0. extern int _PyMutex_TryUnlock(PyMutex *m); +// Yield the processor to other threads (e.g., sched_yield). +extern void _Py_yield(void); + // PyEvent is a one-time event notification typedef struct { diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_long.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_long.h index 36d6d7d70..fb5622c99 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_long.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_long.h @@ -113,9 +113,9 @@ PyAPI_DATA(PyObject*) _PyLong_Rshift(PyObject *, int64_t); // Export for 'math' shared extension PyAPI_DATA(PyObject*) _PyLong_Lshift(PyObject *, int64_t); -PyAPI_FUNC(PyObject*) _PyLong_Add(PyLongObject *left, PyLongObject *right); -PyAPI_FUNC(PyObject*) _PyLong_Multiply(PyLongObject *left, PyLongObject *right); -PyAPI_FUNC(PyObject*) _PyLong_Subtract(PyLongObject *left, PyLongObject *right); +PyAPI_FUNC(_PyStackRef) _PyCompactLong_Add(PyLongObject *left, PyLongObject *right); +PyAPI_FUNC(_PyStackRef) _PyCompactLong_Multiply(PyLongObject *left, PyLongObject *right); +PyAPI_FUNC(_PyStackRef) _PyCompactLong_Subtract(PyLongObject *left, PyLongObject *right); // Export for 'binascii' shared extension. PyAPI_DATA(unsigned char) _PyLong_DigitValue[256]; @@ -136,7 +136,7 @@ extern int _PyLong_FormatWriter( int alternate); extern char* _PyLong_FormatBytesWriter( - _PyBytesWriter *writer, + PyBytesWriter *writer, char *str, PyObject *obj, int base, @@ -214,7 +214,6 @@ _PyLong_BothAreCompact(const PyLongObject* a, const PyLongObject* b) { assert(PyLong_Check(b)); return (a->long_value.lv_tag | b->long_value.lv_tag) < (2 << NON_SIZE_BITS); } - static inline bool _PyLong_IsZero(const PyLongObject *op) { @@ -239,11 +238,12 @@ _PyLong_IsSmallInt(const PyLongObject *op) { assert(PyLong_Check(op)); bool is_small_int = (op->long_value.lv_tag & IMMORTALITY_BIT_MASK) != 0; - assert(PyLong_CheckExact(op) || (!is_small_int)); - assert(_Py_IsImmortal(op) || (!is_small_int)); - assert((_PyLong_IsCompact(op) - && _PY_IS_SMALL_INT(_PyLong_CompactValue(op))) - || (!is_small_int)); + if (is_small_int) { + assert(PyLong_CheckExact(op)); + assert(_Py_IsImmortal(op)); + assert((_PyLong_IsCompact(op) + && _PY_IS_SMALL_INT(_PyLong_CompactValue(op)))); + } return is_small_int; } @@ -286,6 +286,14 @@ _PyLong_SameSign(const PyLongObject *a, const PyLongObject *b) return (a->long_value.lv_tag & SIGN_MASK) == (b->long_value.lv_tag & SIGN_MASK); } +/* Initialize the tag of a freshly-allocated int. */ +static inline void +_PyLong_InitTag(PyLongObject *op) +{ + assert(PyLong_Check(op)); + op->long_value.lv_tag = SIGN_ZERO; /* non-immortal zero */ +} + #define TAG_FROM_SIGN_AND_SIZE(sign, size) \ ((uintptr_t)(1 - (sign)) | ((uintptr_t)(size) << NON_SIZE_BITS)) @@ -295,6 +303,7 @@ _PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) assert(size >= 0); assert(-1 <= sign && sign <= 1); assert(sign != 0 || size == 0); + assert(!_PyLong_IsSmallInt(op)); op->long_value.lv_tag = TAG_FROM_SIGN_AND_SIZE(sign, size); } @@ -302,6 +311,7 @@ static inline void _PyLong_SetDigitCount(PyLongObject *op, Py_ssize_t size) { assert(size >= 0); + assert(!_PyLong_IsSmallInt(op)); op->long_value.lv_tag = (((size_t)size) << NON_SIZE_BITS) | (op->long_value.lv_tag & SIGN_MASK); } @@ -330,6 +340,12 @@ _PyLong_FlipSign(PyLongObject *op) #define _PyLong_FALSE_TAG TAG_FROM_SIGN_AND_SIZE(0, 0) #define _PyLong_TRUE_TAG TAG_FROM_SIGN_AND_SIZE(1, 1) +static inline int +_PyLong_CheckExactAndCompact(PyObject *op) +{ + return PyLong_CheckExact(op) && _PyLong_IsCompact((const PyLongObject *)op); +} + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_magic_number.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_magic_number.h index f31951057..177938e3c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_magic_number.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_magic_number.h @@ -280,8 +280,26 @@ Known values: Python 3.14b3 3625 (Fix handling of opcodes that may leave operands on the stack when optimizing LOAD_FAST) Python 3.14rc2 3626 (Fix missing exception handlers in logical expression) Python 3.14rc3 3627 (Fix miscompilation of some module-level annotations) + Python 3.15a0 3650 (Initial version) + Python 3.15a1 3651 (Simplify LOAD_CONST) + Python 3.15a1 3652 (Virtual iterators) + Python 3.15a1 3653 (Fix handling of opcodes that may leave operands on the stack when optimizing LOAD_FAST) + Python 3.15a1 3654 (Fix missing exception handlers in logical expression) + Python 3.15a1 3655 (Fix miscompilation of some module-level annotations) + Python 3.15a1 3656 (Add TRACE_RECORD instruction, for platforms with switch based interpreter) + Python 3.15a4 3657 (Add BINARY_OP_SUBSCR_USTR_INT) + Python 3.15a4 3658 (Optimize bytecode for list/set called on genexp) + Python 3.15a4 3659 (Add CALL_FUNCTION_EX specialization) + Python 3.15a4 3660 (Change generator preamble code) + Python 3.15a4 3661 (Lazy imports IMPORT_NAME opcode changes) + Python 3.15a8 3662 (Add counter to RESUME) + Python 3.15a8 3663 (Merge GET_ITER and GET_YIELD_FROM_ITER. Modify SEND to make it a bit more like FOR_ITER) + Python 3.15a8 3664 (Fix __qualname__ for __annotate__ functions) + Python 3.15a8 3665 (Add FOR_ITER_VIRTUAL and GET_ITER specializations) + Python 3.15b1 3666 (Add SEND_VIRTUAL and SEND_ASYNC_GEN specializations) - Python 3.15 will start with 3650 + + Python 3.16 will start with 3700 Please don't copy-paste the same pre-release tag for new entries above!!! You should always use the *upcoming* tag. For example, if 3.12a6 came out @@ -292,7 +310,7 @@ PC/launcher.c must also be updated. */ -#define PYC_MAGIC_NUMBER 3627 +#define PYC_MAGIC_NUMBER 3666 /* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes (little-endian) and then appending b'\r\n'. */ #define PYC_MAGIC_NUMBER_TOKEN \ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_mmap.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_mmap.h new file mode 100644 index 000000000..c117cbd16 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_mmap.h @@ -0,0 +1,47 @@ +#ifndef Py_INTERNAL_MMAP_H +#define Py_INTERNAL_MMAP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include "pycore_pystate.h" + +#if defined(_Py_HAVE_PR_SET_VMA_ANON_NAME) && defined(__linux__) +# include +# include +#endif + +#if defined(_Py_HAVE_PR_SET_VMA_ANON_NAME) && defined(__linux__) +static inline int +_PyAnnotateMemoryMap(void *addr, size_t size, const char *name) +{ +#ifndef Py_DEBUG + if (!_Py_GetConfig()->dev_mode) { + return 0; + } +#endif + // The name length cannot exceed 80 (including the '\0'). + assert(strlen(name) < 80); + int res = prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, (unsigned long)addr, size, name); + if (res < 0) { + return -1; + } + return 0; +} +#else +static inline int +_PyAnnotateMemoryMap(void *Py_UNUSED(addr), size_t Py_UNUSED(size), const char *Py_UNUSED(name)) +{ + return 0; +} +#endif + +#ifdef __cplusplus +} +#endif +#endif // !Py_INTERNAL_MMAP_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_modsupport.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_modsupport.h index 614e9f937..d90f42e9c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_modsupport.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_modsupport.h @@ -27,9 +27,8 @@ PyAPI_FUNC(int) _PyArg_NoKeywords(const char *funcname, PyObject *kwargs); // Export for 'zlib' shared extension PyAPI_FUNC(int) _PyArg_CheckPositional(const char *, Py_ssize_t, Py_ssize_t, Py_ssize_t); -#define _Py_ANY_VARARGS(n) ((n) == PY_SSIZE_T_MAX) #define _PyArg_CheckPositional(funcname, nargs, min, max) \ - ((!_Py_ANY_VARARGS(max) && (min) <= (nargs) && (nargs) <= (max)) \ + (((min) <= (nargs) && (nargs) <= (max)) \ || _PyArg_CheckPositional((funcname), (nargs), (min), (max))) extern PyObject ** _Py_VaBuildStack( diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_moduleobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_moduleobject.h index b170d7bce..5bcfd17ce 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_moduleobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_moduleobject.h @@ -1,5 +1,8 @@ #ifndef Py_INTERNAL_MODULEOBJECT_H #define Py_INTERNAL_MODULEOBJECT_H + +#include + #ifdef __cplusplus extern "C" { #endif @@ -16,32 +19,53 @@ extern int _PyModule_IsPossiblyShadowing(PyObject *); extern int _PyModule_IsExtension(PyObject *obj); +extern int _PyModule_InitModuleDictWatcher(PyInterpreterState *interp); + +typedef int (*_Py_modexecfunc)(PyObject *); + typedef struct { PyObject_HEAD PyObject *md_dict; - PyModuleDef *md_def; void *md_state; PyObject *md_weaklist; // for logging purposes after md_dict is cleared PyObject *md_name; + bool md_token_is_def; /* if true, `md_token` is the PyModuleDef */ #ifdef Py_GIL_DISABLED - void *md_gil; + bool md_requires_gil; #endif + Py_ssize_t md_state_size; + traverseproc md_state_traverse; + inquiry md_state_clear; + freefunc md_state_free; + void *md_token; + _Py_modexecfunc md_exec; /* only set if md_token_is_def is true */ } PyModuleObject; -static inline PyModuleDef* _PyModule_GetDef(PyObject *mod) { - assert(PyModule_Check(mod)); - return ((PyModuleObject *)mod)->md_def; +#define _PyModule_CAST(op) \ + (assert(PyModule_Check(op)), _Py_CAST(PyModuleObject*, (op))) + +static inline PyModuleDef *_PyModule_GetDefOrNull(PyObject *arg) { + PyModuleObject *mod = _PyModule_CAST(arg); + if (mod->md_token_is_def) { + return (PyModuleDef *)mod->md_token; + } + return NULL; +} + +// Get md_token. Used in _DuringGC functions; must have no side effects. +static inline PyModuleDef *_PyModule_GetToken(PyObject *arg) { + PyModuleObject *mod = _PyModule_CAST(arg); + return (PyModuleDef *)mod->md_token; } +// Get md_state. Used in _DuringGC functions; must have no side effects. static inline void* _PyModule_GetState(PyObject* mod) { - assert(PyModule_Check(mod)); - return ((PyModuleObject *)mod)->md_state; + return _PyModule_CAST(mod)->md_state; } static inline PyObject* _PyModule_GetDict(PyObject *mod) { - assert(PyModule_Check(mod)); - PyObject *dict = ((PyModuleObject *)mod) -> md_dict; + PyObject *dict = _PyModule_CAST(mod)->md_dict; // _PyModule_GetDict(mod) must not be used after calling module_clear(mod) assert(dict != NULL); return dict; // borrowed reference diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_object.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_object.h index a25998af9..a15685450 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_object.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_object.h @@ -144,7 +144,7 @@ static inline void _Py_RefcntAdd(PyObject* op, Py_ssize_t n) new_refcnt = _Py_IMMORTAL_INITIAL_REFCNT; } # if SIZEOF_VOID_P > 4 - op->ob_refcnt = (PY_UINT32_T)new_refcnt; + op->ob_refcnt = (uint32_t)new_refcnt; # else op->ob_refcnt = new_refcnt; # endif @@ -252,25 +252,6 @@ _Py_DECREF_SPECIALIZED(PyObject *op, const destructor destruct) } } -static inline void -_Py_DECREF_NO_DEALLOC(PyObject *op) -{ - if (_Py_IsImmortal(op)) { - _Py_DECREF_IMMORTAL_STAT_INC(); - return; - } - _Py_DECREF_STAT_INC(); -#ifdef Py_REF_DEBUG - _Py_DEC_REFTOTAL(PyInterpreterState_Get()); -#endif - op->ob_refcnt--; -#ifdef Py_DEBUG - if (op->ob_refcnt <= 0) { - _Py_FatalRefcountError("Expected a positive remaining refcount"); - } -#endif -} - #else // TODO: implement Py_DECREF specializations for Py_GIL_DISABLED build static inline void @@ -279,12 +260,6 @@ _Py_DECREF_SPECIALIZED(PyObject *op, const destructor destruct) Py_DECREF(op); } -static inline void -_Py_DECREF_NO_DEALLOC(PyObject *op) -{ - Py_DECREF(op); -} - static inline int _Py_REF_IS_MERGED(Py_ssize_t ob_ref_shared) { @@ -313,7 +288,7 @@ extern int _PyDict_CheckConsistency(PyObject *mp, int check_content); // Fast inlined version of PyType_HasFeature() static inline int _PyType_HasFeature(PyTypeObject *type, unsigned long feature) { - return ((FT_ATOMIC_LOAD_ULONG_RELAXED(type->tp_flags) & feature) != 0); + return ((type->tp_flags) & feature) != 0; } extern void _PyType_InitCache(PyInterpreterState *interp); @@ -496,6 +471,9 @@ static inline void Py_DECREF_MORTAL_SPECIALIZED(PyObject *op, destructor destruc #define Py_DECREF_MORTAL_SPECIALIZED(op, destruct) Py_DECREF_MORTAL_SPECIALIZED(_PyObject_CAST(op), destruct) #endif +#else // Py_GIL_DISABLED +# define Py_DECREF_MORTAL(op) Py_DECREF(op) +# define Py_DECREF_MORTAL_SPECIALIZED(op, destruct) Py_DECREF(op) #endif /* Inline functions trading binary compatibility for speed: @@ -767,6 +745,27 @@ _Py_TryIncref(PyObject *op) #endif } +// Enqueue an object to be freed possibly after some delay +#ifdef Py_GIL_DISABLED +PyAPI_FUNC(void) _PyObject_XDecRefDelayed(PyObject *obj); +#else +static inline void _PyObject_XDecRefDelayed(PyObject *obj) +{ + Py_XDECREF(obj); +} +#endif + +#ifdef Py_GIL_DISABLED +// Same as `Py_XSETREF` but in free-threading, it stores the object atomically +// and queues the old object to be decrefed at a safe point using QSBR. +PyAPI_FUNC(void) _PyObject_XSetRefDelayed(PyObject **p_obj, PyObject *obj); +#else +static inline void _PyObject_XSetRefDelayed(PyObject **p_obj, PyObject *obj) +{ + Py_XSETREF(*p_obj, obj); +} +#endif + #ifdef Py_REF_DEBUG extern void _PyInterpreterState_FinalizeRefTotal(PyInterpreterState *); extern void _Py_FinalizeRefTotal(_PyRuntimeState *); @@ -842,8 +841,7 @@ static inline Py_hash_t _PyObject_HashFast(PyObject *op) { if (PyUnicode_CheckExact(op)) { - Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED( - _PyASCIIObject_CAST(op)->hash); + Py_hash_t hash = PyUnstable_Unicode_GET_CACHED_HASH(op); if (hash != -1) { return hash; } @@ -882,14 +880,16 @@ PyAPI_FUNC(PyObject *) _PyType_NewManagedObject(PyTypeObject *type); extern PyTypeObject* _PyType_CalculateMetaclass(PyTypeObject *, PyObject *); extern PyObject* _PyType_GetDocFromInternalDoc(const char *, const char *); extern PyObject* _PyType_GetTextSignatureFromInternalDoc(const char *, const char *, int); -extern int _PyObject_SetAttributeErrorContext(PyObject *v, PyObject* name); +// Exported for external JIT support +PyAPI_FUNC(int) _PyObject_SetAttributeErrorContext(PyObject *v, PyObject* name); void _PyObject_InitInlineValues(PyObject *obj, PyTypeObject *tp); extern int _PyObject_StoreInstanceAttribute(PyObject *obj, PyObject *name, PyObject *value); extern bool _PyObject_TryGetInstanceAttribute(PyObject *obj, PyObject *name, PyObject **attr); -extern PyObject *_PyType_LookupRefAndVersion(PyTypeObject *, PyObject *, +// Exported for external JIT support +PyAPI_FUNC(PyObject *) _PyType_LookupRefAndVersion(PyTypeObject *, PyObject *, unsigned int *); // Internal API to look for a name through the MRO. @@ -901,6 +901,10 @@ _PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef extern int _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, PyObject *name, _PyStackRef *method); +// Like PyObject_GetAttr but returns a _PyStackRef. For types, this can +// return a deferred reference to reduce reference count contention. +PyAPI_FUNC(_PyStackRef) _PyObject_GetAttrStackRef(PyObject *obj, PyObject *name); + // Cache the provided init method in the specialization cache of type if the // provided type version matches the current version of the type. // @@ -909,7 +913,9 @@ extern int _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, // deferred reference counting. // // Returns 1 if the value was cached or 0 otherwise. -extern int _PyType_CacheInitForSpecialization(PyHeapTypeObject *type, +// +// Exported for external JIT support +PyAPI_FUNC(int) _PyType_CacheInitForSpecialization(PyHeapTypeObject *type, PyObject *init, unsigned int tp_version); @@ -1011,7 +1017,8 @@ enum _PyAnnotateFormat { _Py_ANNOTATE_FORMAT_STRING = 4, }; -int _PyObject_SetDict(PyObject *obj, PyObject *value); +extern int _PyObject_SetDict(PyObject *obj, PyObject *value); +extern int _PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict); #ifndef Py_GIL_DISABLED static inline Py_ALWAYS_INLINE void _Py_INCREF_MORTAL(PyObject *op) @@ -1025,8 +1032,14 @@ static inline Py_ALWAYS_INLINE void _Py_INCREF_MORTAL(PyObject *op) } #endif } +#else +# define _Py_INCREF_MORTAL(op) Py_INCREF(op) #endif +/* Utility for the tp_traverse slot of mutable heap types that have no other + * references. */ +PyAPI_FUNC(int) _PyObject_VisitType(PyObject *op, visitproc visit, void *arg); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_obmalloc.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_obmalloc.h index a7ba8f340..f6b4bd90a 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_obmalloc.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_obmalloc.h @@ -14,6 +14,12 @@ typedef unsigned int pymem_uint; /* assuming >= 16 bits */ #undef uint #define uint pymem_uint +/* NOTE: the following overviews were in the initial checkin, in 1998. In + * 2026, they're still helpful, but some details have changed. For example, + * we now use 32 size classes 16 bytes apart, and an arena is generally at + * least 1MB. Use sys._debugmallocstats() to see exact current details for + * the specific version of CPython used. + */ /* An object allocator for Python. @@ -208,7 +214,11 @@ typedef unsigned int pymem_uint; /* assuming >= 16 bits */ * mappings to reduce heap fragmentation. */ #ifdef USE_LARGE_ARENAS -#define ARENA_BITS 20 /* 1 MiB */ +# ifdef PYMALLOC_USE_HUGEPAGES +# define ARENA_BITS 21 /* 2 MiB */ +# else +# define ARENA_BITS 20 /* 1 MiB */ +# endif #else #define ARENA_BITS 18 /* 256 KiB */ #endif @@ -469,7 +479,7 @@ nfp free pools in usable_arenas. */ /* How many arena_objects do we initially allocate? - * 16 = can allocate 16 arenas = 16 * ARENA_SIZE = 4MB before growing the + * 16 = can allocate 16 arenas = 16 * ARENA_SIZE before growing the * `arenas` vector. */ #define INITIAL_ARENA_OBJECTS 16 @@ -512,7 +522,11 @@ struct _obmalloc_mgmt { memory address bit allocation for keys - 64-bit pointers, IGNORE_BITS=0 and 2^20 arena size: + ARENA_BITS is configurable: 20 (1 MiB) by default on 64-bit, or + 21 (2 MiB) when PYMALLOC_USE_HUGEPAGES is enabled. All bit widths + below are derived from ARENA_BITS automatically. + + 64-bit pointers, IGNORE_BITS=0 and 2^20 arena size (default): 15 -> MAP_TOP_BITS 15 -> MAP_MID_BITS 14 -> MAP_BOT_BITS @@ -520,6 +534,14 @@ struct _obmalloc_mgmt { ---- 64 + 64-bit pointers, IGNORE_BITS=0 and 2^21 arena size (hugepages): + 15 -> MAP_TOP_BITS + 15 -> MAP_MID_BITS + 13 -> MAP_BOT_BITS + 21 -> ideal aligned arena + ---- + 64 + 64-bit pointers, IGNORE_BITS=16, and 2^20 arena size: 16 -> IGNORE_BITS 10 -> MAP_TOP_BITS @@ -675,7 +697,11 @@ struct _obmalloc_state { /* Allocate memory directly from the O/S virtual memory system, - * where supported. Otherwise fallback on malloc */ + * where supported. Otherwise fallback on malloc. + * + * Large-page and huge-page backends may round the mapped size up + * internally, so pass the original requested size back to + * _PyObject_VirtualFree(). */ void *_PyObject_VirtualAlloc(size_t size); void _PyObject_VirtualFree(void *, size_t size); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_metadata.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_metadata.h index 273e1d838..7d9babf35 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_metadata.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_metadata.h @@ -66,6 +66,8 @@ int _PyOpcode_num_popped(int opcode, int oparg) { return 2; case BINARY_OP_SUBSCR_TUPLE_INT: return 2; + case BINARY_OP_SUBSCR_USTR_INT: + return 2; case BINARY_OP_SUBTRACT_FLOAT: return 2; case BINARY_OP_SUBTRACT_INT: @@ -106,6 +108,10 @@ int _PyOpcode_num_popped(int opcode, int oparg) { return 2 + oparg; case CALL_BUILTIN_O: return 2 + oparg; + case CALL_EX_NON_PY_GENERAL: + return 4; + case CALL_EX_PY: + return 4; case CALL_FUNCTION_EX: return 4; case CALL_INTRINSIC_1: @@ -113,7 +119,7 @@ int _PyOpcode_num_popped(int opcode, int oparg) { case CALL_INTRINSIC_2: return 2; case CALL_ISINSTANCE: - return 2 + oparg; + return 4; case CALL_KW: return 3 + oparg; case CALL_KW_BOUND_METHOD: @@ -151,7 +157,7 @@ int _PyOpcode_num_popped(int opcode, int oparg) { case CHECK_EXC_MATCH: return 2; case CLEANUP_THROW: - return 3; + return 4; case COMPARE_OP: return 2; case COMPARE_OP_FLOAT: @@ -193,7 +199,7 @@ int _PyOpcode_num_popped(int opcode, int oparg) { case END_FOR: return 1; case END_SEND: - return 2; + return 3; case ENTER_EXECUTOR: return 0; case EXIT_INIT_CHECK: @@ -205,15 +211,17 @@ int _PyOpcode_num_popped(int opcode, int oparg) { case FORMAT_WITH_SPEC: return 2; case FOR_ITER: - return 1; + return 2; case FOR_ITER_GEN: - return 1; + return 2; case FOR_ITER_LIST: - return 1; + return 2; case FOR_ITER_RANGE: - return 1; + return 2; case FOR_ITER_TUPLE: - return 1; + return 2; + case FOR_ITER_VIRTUAL: + return 2; case GET_AITER: return 1; case GET_ANEXT: @@ -222,9 +230,11 @@ int _PyOpcode_num_popped(int opcode, int oparg) { return 1; case GET_ITER: return 1; - case GET_LEN: + case GET_ITER_SELF: return 1; - case GET_YIELD_FROM_ITER: + case GET_ITER_VIRTUAL: + return 1; + case GET_LEN: return 1; case IMPORT_FROM: return 1; @@ -239,11 +249,11 @@ int _PyOpcode_num_popped(int opcode, int oparg) { case INSTRUMENTED_END_ASYNC_FOR: return 2; case INSTRUMENTED_END_FOR: - return 2; + return 3; case INSTRUMENTED_END_SEND: - return 2; + return 3; case INSTRUMENTED_FOR_ITER: - return 1; + return 2; case INSTRUMENTED_INSTRUCTION: return 0; case INSTRUMENTED_JUMP_BACKWARD: @@ -257,7 +267,7 @@ int _PyOpcode_num_popped(int opcode, int oparg) { case INSTRUMENTED_NOT_TAKEN: return 0; case INSTRUMENTED_POP_ITER: - return 1; + return 2; case INSTRUMENTED_POP_JUMP_IF_FALSE: return 1; case INSTRUMENTED_POP_JUMP_IF_NONE: @@ -334,10 +344,6 @@ int _PyOpcode_num_popped(int opcode, int oparg) { return 0; case LOAD_CONST: return 0; - case LOAD_CONST_IMMORTAL: - return 0; - case LOAD_CONST_MORTAL: - return 0; case LOAD_DEREF: return 0; case LOAD_FAST: @@ -399,7 +405,7 @@ int _PyOpcode_num_popped(int opcode, int oparg) { case POP_EXCEPT: return 1; case POP_ITER: - return 1; + return 2; case POP_JUMP_IF_FALSE: return 1; case POP_JUMP_IF_NONE: @@ -424,14 +430,20 @@ int _PyOpcode_num_popped(int opcode, int oparg) { return 0; case RESUME_CHECK: return 0; + case RESUME_CHECK_JIT: + return 0; case RETURN_GENERATOR: return 0; case RETURN_VALUE: return 1; case SEND: - return 2; + return 3; + case SEND_ASYNC_GEN: + return 3; case SEND_GEN: - return 2; + return 3; + case SEND_VIRTUAL: + return 3; case SETUP_ANNOTATIONS: return 0; case SETUP_CLEANUP: @@ -492,6 +504,8 @@ int _PyOpcode_num_popped(int opcode, int oparg) { return 1; case TO_BOOL_STR: return 1; + case TRACE_RECORD: + return 0; case UNARY_INVERT: return 1; case UNARY_NEGATIVE: @@ -536,7 +550,7 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { case BINARY_OP_EXTEND: return 1; case BINARY_OP_INPLACE_ADD_UNICODE: - return 0; + return 1; case BINARY_OP_MULTIPLY_FLOAT: return 1; case BINARY_OP_MULTIPLY_INT: @@ -553,6 +567,8 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { return 1; case BINARY_OP_SUBSCR_TUPLE_INT: return 1; + case BINARY_OP_SUBSCR_USTR_INT: + return 1; case BINARY_OP_SUBTRACT_FLOAT: return 1; case BINARY_OP_SUBTRACT_INT: @@ -593,6 +609,10 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { return 1; case CALL_BUILTIN_O: return 1; + case CALL_EX_NON_PY_GENERAL: + return 1; + case CALL_EX_PY: + return 0; case CALL_FUNCTION_EX: return 1; case CALL_INTRINSIC_1: @@ -612,7 +632,7 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { case CALL_LEN: return 1; case CALL_LIST_APPEND: - return 0; + return 1; case CALL_METHOD_DESCRIPTOR_FAST: return 1; case CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS: @@ -638,7 +658,7 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { case CHECK_EXC_MATCH: return 2; case CLEANUP_THROW: - return 2; + return 3; case COMPARE_OP: return 1; case COMPARE_OP_FLOAT: @@ -692,15 +712,17 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { case FORMAT_WITH_SPEC: return 1; case FOR_ITER: - return 2; + return 3; case FOR_ITER_GEN: - return 1; - case FOR_ITER_LIST: return 2; + case FOR_ITER_LIST: + return 3; case FOR_ITER_RANGE: - return 2; + return 3; case FOR_ITER_TUPLE: - return 2; + return 3; + case FOR_ITER_VIRTUAL: + return 3; case GET_AITER: return 1; case GET_ANEXT: @@ -708,11 +730,13 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { case GET_AWAITABLE: return 1; case GET_ITER: - return 1; + return 2; + case GET_ITER_SELF: + return 2; + case GET_ITER_VIRTUAL: + return 2; case GET_LEN: return 2; - case GET_YIELD_FROM_ITER: - return 1; case IMPORT_FROM: return 2; case IMPORT_NAME: @@ -726,11 +750,11 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { case INSTRUMENTED_END_ASYNC_FOR: return 0; case INSTRUMENTED_END_FOR: - return 1; + return 2; case INSTRUMENTED_END_SEND: return 1; case INSTRUMENTED_FOR_ITER: - return 2; + return 3; case INSTRUMENTED_INSTRUCTION: return 0; case INSTRUMENTED_JUMP_BACKWARD: @@ -792,7 +816,7 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { case LOAD_ATTR_CLASS_WITH_METACLASS_CHECK: return 1 + (oparg & 1); case LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN: - return 1; + return 0; case LOAD_ATTR_INSTANCE_VALUE: return 1 + (oparg & 1); case LOAD_ATTR_METHOD_LAZY_DICT: @@ -821,10 +845,6 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { return 1; case LOAD_CONST: return 1; - case LOAD_CONST_IMMORTAL: - return 1; - case LOAD_CONST_MORTAL: - return 1; case LOAD_DEREF: return 1; case LOAD_FAST: @@ -911,14 +931,20 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { return 0; case RESUME_CHECK: return 0; + case RESUME_CHECK_JIT: + return 0; case RETURN_GENERATOR: return 1; case RETURN_VALUE: return 1; case SEND: - return 2; + return 3; + case SEND_ASYNC_GEN: + return 3; case SEND_GEN: - return 1; + return 2; + case SEND_VIRTUAL: + return 3; case SETUP_ANNOTATIONS: return 0; case SETUP_CLEANUP: @@ -979,6 +1005,8 @@ int _PyOpcode_num_pushed(int opcode, int oparg) { return 1; case TO_BOOL_STR: return 1; + case TRACE_RECORD: + return 0; case UNARY_INVERT: return 1; case UNARY_NEGATIVE: @@ -1036,10 +1064,13 @@ enum InstructionFormat { #define HAS_ESCAPES_FLAG (512) #define HAS_EXIT_FLAG (1024) #define HAS_PURE_FLAG (2048) -#define HAS_PASSTHROUGH_FLAG (4096) -#define HAS_OPARG_AND_1_FLAG (8192) -#define HAS_ERROR_NO_POP_FLAG (16384) -#define HAS_NO_SAVE_IP_FLAG (32768) +#define HAS_SYNC_SP_FLAG (4096) +#define HAS_ERROR_NO_POP_FLAG (8192) +#define HAS_NO_SAVE_IP_FLAG (16384) +#define HAS_PERIODIC_FLAG (32768) +#define HAS_UNPREDICTABLE_JUMP_FLAG (65536) +#define HAS_NEEDS_GUARD_IP_FLAG (131072) +#define HAS_RECORDS_VALUE_FLAG (262144) #define OPCODE_HAS_ARG(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_ARG_FLAG)) #define OPCODE_HAS_CONST(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_CONST_FLAG)) #define OPCODE_HAS_NAME(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_NAME_FLAG)) @@ -1052,10 +1083,13 @@ enum InstructionFormat { #define OPCODE_HAS_ESCAPES(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_ESCAPES_FLAG)) #define OPCODE_HAS_EXIT(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_EXIT_FLAG)) #define OPCODE_HAS_PURE(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_PURE_FLAG)) -#define OPCODE_HAS_PASSTHROUGH(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_PASSTHROUGH_FLAG)) -#define OPCODE_HAS_OPARG_AND_1(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_OPARG_AND_1_FLAG)) +#define OPCODE_HAS_SYNC_SP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_SYNC_SP_FLAG)) #define OPCODE_HAS_ERROR_NO_POP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_ERROR_NO_POP_FLAG)) #define OPCODE_HAS_NO_SAVE_IP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_NO_SAVE_IP_FLAG)) +#define OPCODE_HAS_PERIODIC(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_PERIODIC_FLAG)) +#define OPCODE_HAS_UNPREDICTABLE_JUMP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_UNPREDICTABLE_JUMP_FLAG)) +#define OPCODE_HAS_NEEDS_GUARD_IP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_NEEDS_GUARD_IP_FLAG)) +#define OPCODE_HAS_RECORDS_VALUE(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_RECORDS_VALUE_FLAG)) #define OPARG_SIMPLE 0 #define OPARG_CACHE_1 1 @@ -1072,76 +1106,79 @@ enum InstructionFormat { struct opcode_metadata { uint8_t valid_entry; uint8_t instr_format; - uint16_t flags; + uint32_t flags; }; -extern const struct opcode_metadata _PyOpcode_opcode_metadata[267]; +PyAPI_DATA(const struct opcode_metadata) _PyOpcode_opcode_metadata[267]; #ifdef NEED_OPCODE_METADATA const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { - [BINARY_OP] = { true, INSTR_FMT_IBC0000, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_ADD_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG }, - [BINARY_OP_ADD_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG }, - [BINARY_OP_EXTEND] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_INPLACE_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_LOCAL_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_MULTIPLY_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG }, - [BINARY_OP_MULTIPLY_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_SUBSCR_DICT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_SUBSCR_GETITEM] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG }, - [BINARY_OP_SUBSCR_LIST_INT] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_SUBSCR_LIST_SLICE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_SUBSCR_STR_INT] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [BINARY_OP_SUBSCR_TUPLE_INT] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [BINARY_OP_SUBTRACT_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG }, - [BINARY_OP_SUBTRACT_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [BINARY_OP] = { true, INSTR_FMT_IBC0000, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [BINARY_OP_ADD_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG }, + [BINARY_OP_ADD_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG }, + [BINARY_OP_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG }, + [BINARY_OP_EXTEND] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [BINARY_OP_INPLACE_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_LOCAL_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [BINARY_OP_MULTIPLY_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG }, + [BINARY_OP_MULTIPLY_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG }, + [BINARY_OP_SUBSCR_DICT] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [BINARY_OP_SUBSCR_GETITEM] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [BINARY_OP_SUBSCR_LIST_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [BINARY_OP_SUBSCR_LIST_SLICE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [BINARY_OP_SUBSCR_STR_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG }, + [BINARY_OP_SUBSCR_TUPLE_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [BINARY_OP_SUBSCR_USTR_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG }, + [BINARY_OP_SUBTRACT_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG }, + [BINARY_OP_SUBTRACT_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG }, [BINARY_SLICE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [BUILD_INTERPOLATION] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [BUILD_LIST] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [BUILD_MAP] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [BUILD_SET] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [BUILD_SLICE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG }, - [BUILD_STRING] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG }, + [BUILD_SLICE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [BUILD_STRING] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [BUILD_TEMPLATE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [BUILD_TUPLE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG }, [CACHE] = { true, INSTR_FMT_IX, 0 }, - [CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_ALLOC_AND_ENTER_INIT] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_BOUND_METHOD_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [CALL_BOUND_METHOD_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_BUILTIN_CLASS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_BUILTIN_FAST] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_BUILTIN_FAST_WITH_KEYWORDS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_BUILTIN_O] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_FUNCTION_EX] = { true, INSTR_FMT_IX, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_INTRINSIC_1] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_INTRINSIC_2] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_ISINSTANCE] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_KW_BOUND_METHOD] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [CALL_ALLOC_AND_ENTER_INIT] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_BOUND_METHOD_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_BOUND_METHOD_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_BUILTIN_CLASS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_BUILTIN_FAST] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_BUILTIN_FAST_WITH_KEYWORDS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_BUILTIN_O] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_EX_NON_PY_GENERAL] = { true, INSTR_FMT_IXC, HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_EX_PY] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_FUNCTION_EX] = { true, INSTR_FMT_IXC, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [CALL_INTRINSIC_1] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_INTRINSIC_2] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_ISINSTANCE] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [CALL_KW_BOUND_METHOD] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, [CALL_KW_NON_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_KW_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_LEN] = { true, INSTR_FMT_IXC00, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_LIST_APPEND] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_METHOD_DESCRIPTOR_FAST] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_METHOD_DESCRIPTOR_NOARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_METHOD_DESCRIPTOR_O] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_NON_PY_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_PY_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [CALL_PY_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [CALL_STR_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_TUPLE_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CALL_TYPE_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG }, + [CALL_KW_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_LEN] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_LIST_APPEND] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_METHOD_DESCRIPTOR_FAST] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_METHOD_DESCRIPTOR_NOARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_METHOD_DESCRIPTOR_O] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_NON_PY_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_PY_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_PY_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [CALL_STR_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_TUPLE_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CALL_TYPE_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, [CHECK_EG_MATCH] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [CHECK_EXC_MATCH] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [CLEANUP_THROW] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [COMPARE_OP] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [COMPARE_OP_FLOAT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG }, - [COMPARE_OP_INT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, + [COMPARE_OP_INT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG }, [COMPARE_OP_STR] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG }, - [CONTAINS_OP] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CONTAINS_OP_DICT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [CONTAINS_OP_SET] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [CONTAINS_OP] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CONTAINS_OP_DICT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [CONTAINS_OP_SET] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [CONVERT_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [COPY] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_PURE_FLAG }, [COPY_FREE_VARS] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, @@ -1151,9 +1188,9 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [DELETE_GLOBAL] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [DELETE_NAME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [DELETE_SUBSCR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [DICT_MERGE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [DICT_UPDATE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [DICT_MERGE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [DICT_UPDATE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, [END_FOR] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_NO_SAVE_IP_FLAG }, [END_SEND] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_PURE_FLAG }, [ENTER_EXECUTOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, @@ -1161,30 +1198,32 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [EXTENDED_ARG] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, [FORMAT_SIMPLE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [FORMAT_WITH_SPEC] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [FOR_ITER_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG }, - [FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [FOR_ITER_RANGE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG }, - [FOR_ITER_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [FOR_ITER_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG }, + [FOR_ITER_RANGE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG }, + [FOR_ITER_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG }, + [FOR_ITER_VIRTUAL] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG }, [GET_AITER] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [GET_ANEXT] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [GET_AWAITABLE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [GET_ITER] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [GET_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [GET_ITER_SELF] = { true, INSTR_FMT_IXC, HAS_EXIT_FLAG | HAS_RECORDS_VALUE_FLAG }, + [GET_ITER_VIRTUAL] = { true, INSTR_FMT_IXC, HAS_EXIT_FLAG | HAS_RECORDS_VALUE_FLAG }, [GET_LEN] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [GET_YIELD_FROM_ITER] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [IMPORT_FROM] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [IMPORT_NAME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [INSTRUMENTED_CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [INSTRUMENTED_CALL_FUNCTION_EX] = { true, INSTR_FMT_IX, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [INSTRUMENTED_CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [INSTRUMENTED_END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [INSTRUMENTED_CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [INSTRUMENTED_CALL_FUNCTION_EX] = { true, INSTR_FMT_IXC, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [INSTRUMENTED_CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [INSTRUMENTED_END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, [INSTRUMENTED_END_FOR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NO_SAVE_IP_FLAG }, [INSTRUMENTED_END_SEND] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [INSTRUMENTED_FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [INSTRUMENTED_FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, [INSTRUMENTED_INSTRUCTION] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [INSTRUMENTED_JUMP_BACKWARD] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [INSTRUMENTED_JUMP_FORWARD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, - [INSTRUMENTED_LINE] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG }, + [INSTRUMENTED_LINE] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG }, [INSTRUMENTED_LOAD_SUPER_ATTR] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [INSTRUMENTED_NOT_TAKEN] = { true, INSTR_FMT_IX, 0 }, [INSTRUMENTED_POP_ITER] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG }, @@ -1192,91 +1231,92 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [INSTRUMENTED_POP_JUMP_IF_NONE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ESCAPES_FLAG }, [INSTRUMENTED_POP_JUMP_IF_NOT_NONE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ESCAPES_FLAG }, [INSTRUMENTED_POP_JUMP_IF_TRUE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG }, - [INSTRUMENTED_RESUME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [INSTRUMENTED_RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [INSTRUMENTED_YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [INSTRUMENTED_RESUME] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [INSTRUMENTED_RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [INSTRUMENTED_YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, [INTERPRETER_EXIT] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG }, - [IS_OP] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, + [IS_OP] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ESCAPES_FLAG }, [JUMP_BACKWARD] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [JUMP_BACKWARD_JIT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [JUMP_BACKWARD_NO_INTERRUPT] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG }, [JUMP_BACKWARD_NO_JIT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [JUMP_FORWARD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG }, [LIST_APPEND] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG }, - [LIST_EXTEND] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [LIST_EXTEND] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [LOAD_ATTR] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_ATTR_CLASS] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG }, - [LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG }, - [LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG }, - [LOAD_ATTR_INSTANCE_VALUE] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_ATTR_METHOD_LAZY_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [LOAD_ATTR_METHOD_NO_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG }, - [LOAD_ATTR_METHOD_WITH_VALUES] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [LOAD_ATTR_MODULE] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_ATTR_PROPERTY] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [LOAD_ATTR_SLOT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [LOAD_ATTR_WITH_HINT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_ATTR_CLASS] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_INSTANCE_VALUE] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_METHOD_LAZY_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_METHOD_NO_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_METHOD_WITH_VALUES] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_MODULE] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_PROPERTY] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_SLOT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [LOAD_ATTR_WITH_HINT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [LOAD_BUILD_CLASS] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [LOAD_COMMON_CONSTANT] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, [LOAD_CONST] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_CONST_FLAG }, - [LOAD_CONST_IMMORTAL] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_CONST_FLAG }, - [LOAD_CONST_MORTAL] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_CONST_FLAG }, [LOAD_DEREF] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [LOAD_FAST] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_PURE_FLAG }, [LOAD_FAST_AND_CLEAR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG }, [LOAD_FAST_BORROW] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_PURE_FLAG }, [LOAD_FAST_BORROW_LOAD_FAST_BORROW] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG }, - [LOAD_FAST_CHECK] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_FAST_CHECK] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ERROR_FLAG }, [LOAD_FAST_LOAD_FAST] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG }, [LOAD_FROM_DICT_OR_DEREF] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_FREE_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_FROM_DICT_OR_GLOBALS] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_FROM_DICT_OR_GLOBALS] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [LOAD_GLOBAL] = { true, INSTR_FMT_IBC000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [LOAD_GLOBAL_BUILTIN] = { true, INSTR_FMT_IBC000, HAS_ARG_FLAG | HAS_DEOPT_FLAG }, [LOAD_GLOBAL_MODULE] = { true, INSTR_FMT_IBC000, HAS_ARG_FLAG | HAS_DEOPT_FLAG }, - [LOAD_LOCALS] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_LOCALS] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG }, [LOAD_NAME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [LOAD_SMALL_INT] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, - [LOAD_SPECIAL] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_SPECIAL] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [LOAD_SUPER_ATTR] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_SUPER_ATTR_ATTR] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_SUPER_ATTR_METHOD] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_SUPER_ATTR_ATTR] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_SUPER_ATTR_METHOD] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [MAKE_CELL] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_FREE_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [MAKE_FUNCTION] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [MAKE_FUNCTION] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [MAP_ADD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [MATCH_CLASS] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [MATCH_CLASS] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [MATCH_KEYS] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [MATCH_MAPPING] = { true, INSTR_FMT_IX, 0 }, [MATCH_SEQUENCE] = { true, INSTR_FMT_IX, 0 }, [NOP] = { true, INSTR_FMT_IX, HAS_PURE_FLAG }, [NOT_TAKEN] = { true, INSTR_FMT_IX, HAS_PURE_FLAG }, [POP_EXCEPT] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG }, - [POP_ITER] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_PURE_FLAG }, + [POP_ITER] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG }, [POP_JUMP_IF_FALSE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG }, - [POP_JUMP_IF_NONE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG }, - [POP_JUMP_IF_NOT_NONE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG }, + [POP_JUMP_IF_NONE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ESCAPES_FLAG }, + [POP_JUMP_IF_NOT_NONE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ESCAPES_FLAG }, [POP_JUMP_IF_TRUE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG }, [POP_TOP] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_PURE_FLAG }, [PUSH_EXC_INFO] = { true, INSTR_FMT_IX, 0 }, [PUSH_NULL] = { true, INSTR_FMT_IX, HAS_PURE_FLAG }, - [RAISE_VARARGS] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [RAISE_VARARGS] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG }, [RERAISE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [RESERVED] = { true, INSTR_FMT_IX, 0 }, - [RESUME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [RESUME_CHECK] = { true, INSTR_FMT_IX, HAS_DEOPT_FLAG }, - [RETURN_GENERATOR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG }, - [SEND] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [SEND_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG }, + [RESUME] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [RESUME_CHECK] = { true, INSTR_FMT_IXC, HAS_DEOPT_FLAG }, + [RESUME_CHECK_JIT] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG }, + [RETURN_GENERATOR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [SEND] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, + [SEND_ASYNC_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG }, + [SEND_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG | HAS_RECORDS_VALUE_FLAG }, + [SEND_VIRTUAL] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG }, [SETUP_ANNOTATIONS] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [SET_ADD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [SET_FUNCTION_ATTRIBUTE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [SET_UPDATE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [SET_FUNCTION_ATTRIBUTE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG }, + [SET_UPDATE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, [STORE_ATTR] = { true, INSTR_FMT_IBC000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [STORE_ATTR_INSTANCE_VALUE] = { true, INSTR_FMT_IXC000, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [STORE_ATTR_SLOT] = { true, INSTR_FMT_IXC000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [STORE_ATTR_WITH_HINT] = { true, INSTR_FMT_IBC000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [STORE_ATTR_INSTANCE_VALUE] = { true, INSTR_FMT_IXC000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [STORE_ATTR_SLOT] = { true, INSTR_FMT_IXC000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, + [STORE_ATTR_WITH_HINT] = { true, INSTR_FMT_IBC000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [STORE_DEREF] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_FREE_FLAG | HAS_ESCAPES_FLAG }, [STORE_FAST] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG }, [STORE_FAST_LOAD_FAST] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG }, @@ -1285,26 +1325,27 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [STORE_NAME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [STORE_SLICE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [STORE_SUBSCR] = { true, INSTR_FMT_IXC, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [STORE_SUBSCR_DICT] = { true, INSTR_FMT_IXC, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [STORE_SUBSCR_DICT] = { true, INSTR_FMT_IXC, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [STORE_SUBSCR_LIST_INT] = { true, INSTR_FMT_IXC, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, [SWAP] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_PURE_FLAG }, [TO_BOOL] = { true, INSTR_FMT_IXC00, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [TO_BOOL_ALWAYS_TRUE] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [TO_BOOL_ALWAYS_TRUE] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [TO_BOOL_BOOL] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG }, - [TO_BOOL_INT] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [TO_BOOL_LIST] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG }, + [TO_BOOL_INT] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG }, + [TO_BOOL_LIST] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, [TO_BOOL_NONE] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG }, - [TO_BOOL_STR] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, - [UNARY_INVERT] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [UNARY_NEGATIVE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [UNARY_NOT] = { true, INSTR_FMT_IX, HAS_PURE_FLAG }, + [TO_BOOL_STR] = { true, INSTR_FMT_IXC00, HAS_EXIT_FLAG }, + [TRACE_RECORD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [UNARY_INVERT] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [UNARY_NEGATIVE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, + [UNARY_NOT] = { true, INSTR_FMT_IX, 0 }, [UNPACK_EX] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [UNPACK_SEQUENCE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [UNPACK_SEQUENCE_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [UNPACK_SEQUENCE_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG }, - [UNPACK_SEQUENCE_TWO_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [UNPACK_SEQUENCE_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [UNPACK_SEQUENCE_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, + [UNPACK_SEQUENCE_TWO_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, [WITH_EXCEPT_START] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, - [YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ESCAPES_FLAG }, + [YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, [ANNOTATIONS_PLACEHOLDER] = { true, -1, HAS_PURE_FLAG }, [JUMP] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, [JUMP_IF_FALSE] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, @@ -1319,7 +1360,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { }; #endif -#define MAX_UOP_PER_EXPANSION 10 +#define MAX_UOP_PER_EXPANSION 11 struct opcode_macro_expansion { int nuops; struct { int16_t uop; int8_t size; int8_t offset; } uops[MAX_UOP_PER_EXPANSION]; @@ -1329,22 +1370,23 @@ extern const struct opcode_macro_expansion _PyOpcode_macro_expansion[256]; #ifdef NEED_OPCODE_METADATA const struct opcode_macro_expansion _PyOpcode_macro_expansion[256] = { - [BINARY_OP] = { .nuops = 1, .uops = { { _BINARY_OP, OPARG_SIMPLE, 4 } } }, - [BINARY_OP_ADD_FLOAT] = { .nuops = 3, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_FLOAT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_ADD_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_INT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_ADD_UNICODE] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_UNICODE, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_EXTEND] = { .nuops = 2, .uops = { { _GUARD_BINARY_OP_EXTEND, 4, 1 }, { _BINARY_OP_EXTEND, 4, 1 } } }, + [BINARY_OP] = { .nuops = 5, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 0 }, { _RECORD_NOS_TYPE, OPARG_SIMPLE, 0 }, { _BINARY_OP, OPARG_SIMPLE, 4 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _POP_TOP, OPARG_SIMPLE, 4 } } }, + [BINARY_OP_ADD_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_ADD_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_ADD_UNICODE] = { .nuops = 5, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_UNICODE, OPARG_SIMPLE, 5 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 5 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_EXTEND] = { .nuops = 4, .uops = { { _GUARD_BINARY_OP_EXTEND, 4, 1 }, { _BINARY_OP_EXTEND, 4, 1 }, { _POP_TOP, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, [BINARY_OP_INPLACE_ADD_UNICODE] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_INPLACE_ADD_UNICODE, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_MULTIPLY_FLOAT] = { .nuops = 3, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_FLOAT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_MULTIPLY_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_INT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBSCR_DICT] = { .nuops = 2, .uops = { { _GUARD_NOS_DICT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_DICT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBSCR_GETITEM] = { .nuops = 4, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_CHECK_FUNC, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_INIT_CALL, OPARG_SIMPLE, 5 }, { _PUSH_FRAME, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBSCR_LIST_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_INT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBSCR_LIST_SLICE] = { .nuops = 3, .uops = { { _GUARD_TOS_SLICE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_SLICE, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBSCR_STR_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_STR_INT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBSCR_TUPLE_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_TUPLE, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_TUPLE_INT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBTRACT_FLOAT] = { .nuops = 3, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBTRACT_FLOAT, OPARG_SIMPLE, 5 } } }, - [BINARY_OP_SUBTRACT_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBTRACT_INT, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_MULTIPLY_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_MULTIPLY_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_DICT] = { .nuops = 5, .uops = { { _RECORD_NOS_TYPE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_DICT_SUBSCRIPT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_DICT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_GETITEM] = { .nuops = 5, .uops = { { _RECORD_NOS, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_CHECK_FUNC, OPARG_SIMPLE, 5 }, { _BINARY_OP_SUBSCR_INIT_CALL, OPARG_SIMPLE, 5 }, { _PUSH_FRAME, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_LIST_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_LIST_SLICE] = { .nuops = 5, .uops = { { _GUARD_TOS_SLICE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_LIST_SLICE, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_STR_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_COMPACT_ASCII, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_STR_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_TUPLE_INT] = { .nuops = 6, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_TUPLE, OPARG_SIMPLE, 0 }, { _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_TUPLE_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBSCR_USTR_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBSCR_USTR_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBTRACT_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBTRACT_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 5 } } }, + [BINARY_OP_SUBTRACT_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_SUBTRACT_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 }, { _POP_TOP_INT, OPARG_SIMPLE, 5 } } }, [BINARY_SLICE] = { .nuops = 1, .uops = { { _BINARY_SLICE, OPARG_SIMPLE, 0 } } }, [BUILD_INTERPOLATION] = { .nuops = 1, .uops = { { _BUILD_INTERPOLATION, OPARG_SIMPLE, 0 } } }, [BUILD_LIST] = { .nuops = 1, .uops = { { _BUILD_LIST, OPARG_SIMPLE, 0 } } }, @@ -1354,40 +1396,42 @@ _PyOpcode_macro_expansion[256] = { [BUILD_STRING] = { .nuops = 1, .uops = { { _BUILD_STRING, OPARG_SIMPLE, 0 } } }, [BUILD_TEMPLATE] = { .nuops = 1, .uops = { { _BUILD_TEMPLATE, OPARG_SIMPLE, 0 } } }, [BUILD_TUPLE] = { .nuops = 1, .uops = { { _BUILD_TUPLE, OPARG_SIMPLE, 0 } } }, - [CALL_ALLOC_AND_ENTER_INIT] = { .nuops = 4, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_AND_ALLOCATE_OBJECT, 2, 1 }, { _CREATE_INIT_FRAME, OPARG_SIMPLE, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, - [CALL_BOUND_METHOD_EXACT_ARGS] = { .nuops = 10, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_CALL_BOUND_METHOD_EXACT_ARGS, OPARG_SIMPLE, 1 }, { _INIT_CALL_BOUND_METHOD_EXACT_ARGS, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION, 2, 1 }, { _CHECK_FUNCTION_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _CHECK_STACK_SPACE, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _INIT_CALL_PY_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, - [CALL_BOUND_METHOD_GENERAL] = { .nuops = 7, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_METHOD_VERSION, 2, 1 }, { _EXPAND_METHOD, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _PY_FRAME_GENERAL, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, - [CALL_BUILTIN_CLASS] = { .nuops = 2, .uops = { { _CALL_BUILTIN_CLASS, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_BUILTIN_FAST] = { .nuops = 2, .uops = { { _CALL_BUILTIN_FAST, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_BUILTIN_FAST_WITH_KEYWORDS] = { .nuops = 2, .uops = { { _CALL_BUILTIN_FAST_WITH_KEYWORDS, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_BUILTIN_O] = { .nuops = 2, .uops = { { _CALL_BUILTIN_O, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_INTRINSIC_1] = { .nuops = 1, .uops = { { _CALL_INTRINSIC_1, OPARG_SIMPLE, 0 } } }, - [CALL_INTRINSIC_2] = { .nuops = 1, .uops = { { _CALL_INTRINSIC_2, OPARG_SIMPLE, 0 } } }, - [CALL_ISINSTANCE] = { .nuops = 1, .uops = { { _CALL_ISINSTANCE, OPARG_SIMPLE, 3 } } }, - [CALL_KW_BOUND_METHOD] = { .nuops = 6, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_METHOD_VERSION_KW, 2, 1 }, { _EXPAND_METHOD_KW, OPARG_SIMPLE, 3 }, { _PY_FRAME_KW, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, - [CALL_KW_NON_PY] = { .nuops = 3, .uops = { { _CHECK_IS_NOT_PY_CALLABLE_KW, OPARG_SIMPLE, 3 }, { _CALL_KW_NON_PY, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_KW_PY] = { .nuops = 6, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION_KW, 2, 1 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _PY_FRAME_KW, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, - [CALL_LEN] = { .nuops = 3, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_LEN, OPARG_SIMPLE, 3 }, { _CALL_LEN, OPARG_SIMPLE, 3 } } }, - [CALL_LIST_APPEND] = { .nuops = 1, .uops = { { _CALL_LIST_APPEND, OPARG_SIMPLE, 3 } } }, - [CALL_METHOD_DESCRIPTOR_FAST] = { .nuops = 2, .uops = { { _CALL_METHOD_DESCRIPTOR_FAST, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = { .nuops = 2, .uops = { { _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_METHOD_DESCRIPTOR_NOARGS] = { .nuops = 2, .uops = { { _CALL_METHOD_DESCRIPTOR_NOARGS, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_METHOD_DESCRIPTOR_O] = { .nuops = 2, .uops = { { _CALL_METHOD_DESCRIPTOR_O, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_NON_PY_GENERAL] = { .nuops = 3, .uops = { { _CHECK_IS_NOT_PY_CALLABLE, OPARG_SIMPLE, 3 }, { _CALL_NON_PY_GENERAL, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_PY_EXACT_ARGS] = { .nuops = 8, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION, 2, 1 }, { _CHECK_FUNCTION_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _CHECK_STACK_SPACE, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _INIT_CALL_PY_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, - [CALL_PY_GENERAL] = { .nuops = 6, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION, 2, 1 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _PY_FRAME_GENERAL, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, - [CALL_STR_1] = { .nuops = 4, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_STR_1, OPARG_SIMPLE, 3 }, { _CALL_STR_1, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_TUPLE_1] = { .nuops = 4, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_TUPLE_1, OPARG_SIMPLE, 3 }, { _CALL_TUPLE_1, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC, OPARG_SIMPLE, 3 } } }, - [CALL_TYPE_1] = { .nuops = 3, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_TYPE_1, OPARG_SIMPLE, 3 }, { _CALL_TYPE_1, OPARG_SIMPLE, 3 } } }, + [CALL_ALLOC_AND_ENTER_INIT] = { .nuops = 7, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_OBJECT, 2, 1 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _ALLOCATE_OBJECT, OPARG_SIMPLE, 3 }, { _CREATE_INIT_FRAME, OPARG_SIMPLE, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_BOUND_METHOD_EXACT_ARGS] = { .nuops = 11, .uops = { { _RECORD_BOUND_METHOD, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_CALL_BOUND_METHOD_EXACT_ARGS, OPARG_SIMPLE, 1 }, { _INIT_CALL_BOUND_METHOD_EXACT_ARGS, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION, 2, 1 }, { _CHECK_FUNCTION_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _CHECK_STACK_SPACE, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _INIT_CALL_PY_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_BOUND_METHOD_GENERAL] = { .nuops = 8, .uops = { { _RECORD_BOUND_METHOD, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_METHOD_VERSION, 2, 1 }, { _EXPAND_METHOD, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _PY_FRAME_GENERAL, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_BUILTIN_CLASS] = { .nuops = 6, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_BUILTIN_CLASS, OPARG_SIMPLE, 3 }, { _CALL_BUILTIN_CLASS, OPARG_SIMPLE, 3 }, { _POP_TOP_OPARG, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_BUILTIN_FAST] = { .nuops = 6, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_BUILTIN_FAST, OPARG_SIMPLE, 3 }, { _CALL_BUILTIN_FAST, OPARG_SIMPLE, 3 }, { _POP_TOP_OPARG, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_BUILTIN_FAST_WITH_KEYWORDS] = { .nuops = 6, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS, OPARG_SIMPLE, 3 }, { _CALL_BUILTIN_FAST_WITH_KEYWORDS, OPARG_SIMPLE, 3 }, { _POP_TOP_OPARG, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_BUILTIN_O] = { .nuops = 7, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_BUILTIN_O, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_LIMIT, OPARG_SIMPLE, 3 }, { _CALL_BUILTIN_O, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_EX_NON_PY_GENERAL] = { .nuops = 4, .uops = { { _CHECK_IS_NOT_PY_CALLABLE_EX, OPARG_SIMPLE, 1 }, { _MAKE_CALLARGS_A_TUPLE, OPARG_SIMPLE, 1 }, { _CALL_FUNCTION_EX_NON_PY_GENERAL, OPARG_SIMPLE, 1 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 1 } } }, + [CALL_EX_PY] = { .nuops = 7, .uops = { { _RECORD_4OS, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _MAKE_CALLARGS_A_TUPLE, OPARG_SIMPLE, 1 }, { _CHECK_IS_PY_CALLABLE_EX, OPARG_SIMPLE, 1 }, { _PY_FRAME_EX, OPARG_SIMPLE, 1 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 1 }, { _PUSH_FRAME, OPARG_SIMPLE, 1 } } }, + [CALL_INTRINSIC_1] = { .nuops = 2, .uops = { { _CALL_INTRINSIC_1, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [CALL_INTRINSIC_2] = { .nuops = 3, .uops = { { _CALL_INTRINSIC_2, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [CALL_ISINSTANCE] = { .nuops = 3, .uops = { { _GUARD_THIRD_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_ISINSTANCE, OPARG_SIMPLE, 3 }, { _CALL_ISINSTANCE, OPARG_SIMPLE, 3 } } }, + [CALL_KW_BOUND_METHOD] = { .nuops = 7, .uops = { { _RECORD_CALLABLE_KW, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_METHOD_VERSION_KW, 2, 1 }, { _EXPAND_METHOD_KW, OPARG_SIMPLE, 3 }, { _PY_FRAME_KW, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_KW_NON_PY] = { .nuops = 3, .uops = { { _CHECK_IS_NOT_PY_CALLABLE_KW, OPARG_SIMPLE, 3 }, { _CALL_KW_NON_PY, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_KW_PY] = { .nuops = 7, .uops = { { _RECORD_CALLABLE_KW, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION_KW, 2, 1 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _PY_FRAME_KW, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_LEN] = { .nuops = 5, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_LEN, OPARG_SIMPLE, 3 }, { _CALL_LEN, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 } } }, + [CALL_LIST_APPEND] = { .nuops = 6, .uops = { { _GUARD_CALLABLE_LIST_APPEND, OPARG_SIMPLE, 3 }, { _GUARD_NOS_NOT_NULL, OPARG_SIMPLE, 3 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 3 }, { _CALL_LIST_APPEND, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 } } }, + [CALL_METHOD_DESCRIPTOR_FAST] = { .nuops = 5, .uops = { { _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST, OPARG_SIMPLE, 3 }, { _CALL_METHOD_DESCRIPTOR_FAST, OPARG_SIMPLE, 3 }, { _POP_TOP_OPARG, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = { .nuops = 6, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS, OPARG_SIMPLE, 3 }, { _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS, OPARG_SIMPLE, 3 }, { _POP_TOP_OPARG, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_METHOD_DESCRIPTOR_NOARGS] = { .nuops = 7, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_LIMIT, OPARG_SIMPLE, 3 }, { _CALL_METHOD_DESCRIPTOR_NOARGS, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_METHOD_DESCRIPTOR_O] = { .nuops = 8, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _GUARD_CALLABLE_METHOD_DESCRIPTOR_O, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_LIMIT, OPARG_SIMPLE, 3 }, { _CALL_METHOD_DESCRIPTOR_O, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_NON_PY_GENERAL] = { .nuops = 4, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _CHECK_IS_NOT_PY_CALLABLE, OPARG_SIMPLE, 3 }, { _CALL_NON_PY_GENERAL, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_PY_EXACT_ARGS] = { .nuops = 9, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION, 2, 1 }, { _CHECK_FUNCTION_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _CHECK_STACK_SPACE, OPARG_SIMPLE, 3 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _INIT_CALL_PY_EXACT_ARGS, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_PY_GENERAL] = { .nuops = 7, .uops = { { _RECORD_CALLABLE, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _CHECK_FUNCTION_VERSION, 2, 1 }, { _CHECK_RECURSION_REMAINING, OPARG_SIMPLE, 3 }, { _PY_FRAME_GENERAL, OPARG_SIMPLE, 3 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 3 }, { _PUSH_FRAME, OPARG_SIMPLE, 3 } } }, + [CALL_STR_1] = { .nuops = 5, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_STR_1, OPARG_SIMPLE, 3 }, { _CALL_STR_1, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_TUPLE_1] = { .nuops = 5, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_TUPLE_1, OPARG_SIMPLE, 3 }, { _CALL_TUPLE_1, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 }, { _CHECK_PERIODIC_AT_END, OPARG_REPLACED, 3 } } }, + [CALL_TYPE_1] = { .nuops = 4, .uops = { { _GUARD_NOS_NULL, OPARG_SIMPLE, 3 }, { _GUARD_CALLABLE_TYPE_1, OPARG_SIMPLE, 3 }, { _CALL_TYPE_1, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 } } }, [CHECK_EG_MATCH] = { .nuops = 1, .uops = { { _CHECK_EG_MATCH, OPARG_SIMPLE, 0 } } }, [CHECK_EXC_MATCH] = { .nuops = 1, .uops = { { _CHECK_EXC_MATCH, OPARG_SIMPLE, 0 } } }, [COMPARE_OP] = { .nuops = 1, .uops = { { _COMPARE_OP, OPARG_SIMPLE, 0 } } }, - [COMPARE_OP_FLOAT] = { .nuops = 3, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _COMPARE_OP_FLOAT, OPARG_SIMPLE, 1 } } }, - [COMPARE_OP_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _COMPARE_OP_INT, OPARG_SIMPLE, 1 } } }, - [COMPARE_OP_STR] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _COMPARE_OP_STR, OPARG_SIMPLE, 1 } } }, - [CONTAINS_OP] = { .nuops = 1, .uops = { { _CONTAINS_OP, OPARG_SIMPLE, 0 } } }, - [CONTAINS_OP_DICT] = { .nuops = 2, .uops = { { _GUARD_TOS_DICT, OPARG_SIMPLE, 0 }, { _CONTAINS_OP_DICT, OPARG_SIMPLE, 1 } } }, - [CONTAINS_OP_SET] = { .nuops = 2, .uops = { { _GUARD_TOS_ANY_SET, OPARG_SIMPLE, 0 }, { _CONTAINS_OP_SET, OPARG_SIMPLE, 1 } } }, + [COMPARE_OP_FLOAT] = { .nuops = 5, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _COMPARE_OP_FLOAT, OPARG_SIMPLE, 1 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 1 }, { _POP_TOP_FLOAT, OPARG_SIMPLE, 1 } } }, + [COMPARE_OP_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _COMPARE_OP_INT, OPARG_SIMPLE, 1 }, { _POP_TOP_INT, OPARG_SIMPLE, 1 }, { _POP_TOP_INT, OPARG_SIMPLE, 1 } } }, + [COMPARE_OP_STR] = { .nuops = 5, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _COMPARE_OP_STR, OPARG_SIMPLE, 1 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 1 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 1 } } }, + [CONTAINS_OP] = { .nuops = 3, .uops = { { _CONTAINS_OP, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [CONTAINS_OP_DICT] = { .nuops = 4, .uops = { { _GUARD_TOS_ANY_DICT, OPARG_SIMPLE, 0 }, { _CONTAINS_OP_DICT, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 } } }, + [CONTAINS_OP_SET] = { .nuops = 4, .uops = { { _GUARD_TOS_ANY_SET, OPARG_SIMPLE, 0 }, { _CONTAINS_OP_SET, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 } } }, [CONVERT_VALUE] = { .nuops = 1, .uops = { { _CONVERT_VALUE, OPARG_SIMPLE, 0 } } }, [COPY] = { .nuops = 1, .uops = { { _COPY, OPARG_SIMPLE, 0 } } }, [COPY_FREE_VARS] = { .nuops = 1, .uops = { { _COPY_FREE_VARS, OPARG_SIMPLE, 0 } } }, @@ -1397,46 +1441,51 @@ _PyOpcode_macro_expansion[256] = { [DELETE_GLOBAL] = { .nuops = 1, .uops = { { _DELETE_GLOBAL, OPARG_SIMPLE, 0 } } }, [DELETE_NAME] = { .nuops = 1, .uops = { { _DELETE_NAME, OPARG_SIMPLE, 0 } } }, [DELETE_SUBSCR] = { .nuops = 1, .uops = { { _DELETE_SUBSCR, OPARG_SIMPLE, 0 } } }, - [DICT_MERGE] = { .nuops = 1, .uops = { { _DICT_MERGE, OPARG_SIMPLE, 0 } } }, - [DICT_UPDATE] = { .nuops = 1, .uops = { { _DICT_UPDATE, OPARG_SIMPLE, 0 } } }, + [DICT_MERGE] = { .nuops = 2, .uops = { { _DICT_MERGE, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [DICT_UPDATE] = { .nuops = 2, .uops = { { _DICT_UPDATE, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, [END_FOR] = { .nuops = 1, .uops = { { _END_FOR, OPARG_SIMPLE, 0 } } }, [END_SEND] = { .nuops = 1, .uops = { { _END_SEND, OPARG_SIMPLE, 0 } } }, [EXIT_INIT_CHECK] = { .nuops = 1, .uops = { { _EXIT_INIT_CHECK, OPARG_SIMPLE, 0 } } }, [FORMAT_SIMPLE] = { .nuops = 1, .uops = { { _FORMAT_SIMPLE, OPARG_SIMPLE, 0 } } }, [FORMAT_WITH_SPEC] = { .nuops = 1, .uops = { { _FORMAT_WITH_SPEC, OPARG_SIMPLE, 0 } } }, - [FOR_ITER] = { .nuops = 1, .uops = { { _FOR_ITER, OPARG_REPLACED, 0 } } }, - [FOR_ITER_GEN] = { .nuops = 3, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _FOR_ITER_GEN_FRAME, OPARG_SIMPLE, 1 }, { _PUSH_FRAME, OPARG_SIMPLE, 1 } } }, + [FOR_ITER] = { .nuops = 2, .uops = { { _RECORD_NOS_TYPE, OPARG_SIMPLE, 0 }, { _FOR_ITER, OPARG_REPLACED, 0 } } }, + [FOR_ITER_GEN] = { .nuops = 4, .uops = { { _RECORD_NOS_GEN_FUNC, OPARG_SIMPLE, 0 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _FOR_ITER_GEN_FRAME, OPARG_SIMPLE, 1 }, { _PUSH_FRAME, OPARG_SIMPLE, 1 } } }, [FOR_ITER_LIST] = { .nuops = 3, .uops = { { _ITER_CHECK_LIST, OPARG_SIMPLE, 1 }, { _ITER_JUMP_LIST, OPARG_REPLACED, 1 }, { _ITER_NEXT_LIST, OPARG_REPLACED, 1 } } }, [FOR_ITER_RANGE] = { .nuops = 3, .uops = { { _ITER_CHECK_RANGE, OPARG_SIMPLE, 1 }, { _ITER_JUMP_RANGE, OPARG_REPLACED, 1 }, { _ITER_NEXT_RANGE, OPARG_SIMPLE, 1 } } }, [FOR_ITER_TUPLE] = { .nuops = 3, .uops = { { _ITER_CHECK_TUPLE, OPARG_SIMPLE, 1 }, { _ITER_JUMP_TUPLE, OPARG_REPLACED, 1 }, { _ITER_NEXT_TUPLE, OPARG_SIMPLE, 1 } } }, + [FOR_ITER_VIRTUAL] = { .nuops = 2, .uops = { { _GUARD_TOS_NOT_NULL, OPARG_SIMPLE, 1 }, { _FOR_ITER_VIRTUAL, OPARG_REPLACED, 1 } } }, [GET_AITER] = { .nuops = 1, .uops = { { _GET_AITER, OPARG_SIMPLE, 0 } } }, [GET_ANEXT] = { .nuops = 1, .uops = { { _GET_ANEXT, OPARG_SIMPLE, 0 } } }, [GET_AWAITABLE] = { .nuops = 1, .uops = { { _GET_AWAITABLE, OPARG_SIMPLE, 0 } } }, - [GET_ITER] = { .nuops = 1, .uops = { { _GET_ITER, OPARG_SIMPLE, 0 } } }, + [GET_ITER] = { .nuops = 2, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 0 }, { _GET_ITER, OPARG_SIMPLE, 0 } } }, + [GET_ITER_SELF] = { .nuops = 3, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 0 }, { _GUARD_ITERATOR, OPARG_SIMPLE, 1 }, { _PUSH_NULL, OPARG_SIMPLE, 1 } } }, + [GET_ITER_VIRTUAL] = { .nuops = 3, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 0 }, { _GUARD_ITER_VIRTUAL, OPARG_SIMPLE, 1 }, { _PUSH_TAGGED_ZERO, OPARG_SIMPLE, 1 } } }, [GET_LEN] = { .nuops = 1, .uops = { { _GET_LEN, OPARG_SIMPLE, 0 } } }, - [GET_YIELD_FROM_ITER] = { .nuops = 1, .uops = { { _GET_YIELD_FROM_ITER, OPARG_SIMPLE, 0 } } }, [IMPORT_FROM] = { .nuops = 1, .uops = { { _IMPORT_FROM, OPARG_SIMPLE, 0 } } }, [IMPORT_NAME] = { .nuops = 1, .uops = { { _IMPORT_NAME, OPARG_SIMPLE, 0 } } }, - [IS_OP] = { .nuops = 1, .uops = { { _IS_OP, OPARG_SIMPLE, 0 } } }, + [IS_OP] = { .nuops = 3, .uops = { { _IS_OP, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [JUMP_BACKWARD] = { .nuops = 2, .uops = { { _CHECK_PERIODIC, OPARG_SIMPLE, 1 }, { _JUMP_BACKWARD_NO_INTERRUPT, OPARG_REPLACED, 1 } } }, + [JUMP_BACKWARD_NO_INTERRUPT] = { .nuops = 1, .uops = { { _JUMP_BACKWARD_NO_INTERRUPT, OPARG_REPLACED, 0 } } }, + [JUMP_BACKWARD_NO_JIT] = { .nuops = 2, .uops = { { _CHECK_PERIODIC, OPARG_SIMPLE, 1 }, { _JUMP_BACKWARD_NO_INTERRUPT, OPARG_REPLACED, 1 } } }, [LIST_APPEND] = { .nuops = 1, .uops = { { _LIST_APPEND, OPARG_SIMPLE, 0 } } }, - [LIST_EXTEND] = { .nuops = 1, .uops = { { _LIST_EXTEND, OPARG_SIMPLE, 0 } } }, + [LIST_EXTEND] = { .nuops = 2, .uops = { { _LIST_EXTEND, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, [LOAD_ATTR] = { .nuops = 1, .uops = { { _LOAD_ATTR, OPARG_SIMPLE, 8 } } }, - [LOAD_ATTR_CLASS] = { .nuops = 3, .uops = { { _CHECK_ATTR_CLASS, 2, 1 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, - [LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { .nuops = 4, .uops = { { _CHECK_ATTR_CLASS, 2, 1 }, { _GUARD_TYPE_VERSION, 2, 3 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, - [LOAD_ATTR_INSTANCE_VALUE] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_MANAGED_OBJECT_HAS_VALUES, OPARG_SIMPLE, 3 }, { _LOAD_ATTR_INSTANCE_VALUE, 1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, - [LOAD_ATTR_METHOD_LAZY_DICT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_ATTR_METHOD_LAZY_DICT, 1, 3 }, { _LOAD_ATTR_METHOD_LAZY_DICT, 4, 5 } } }, - [LOAD_ATTR_METHOD_NO_DICT] = { .nuops = 2, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_METHOD_NO_DICT, 4, 5 } } }, - [LOAD_ATTR_METHOD_WITH_VALUES] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, OPARG_SIMPLE, 3 }, { _GUARD_KEYS_VERSION, 2, 3 }, { _LOAD_ATTR_METHOD_WITH_VALUES, 4, 5 } } }, - [LOAD_ATTR_MODULE] = { .nuops = 3, .uops = { { _LOAD_ATTR_MODULE, 2, 1 }, { _LOAD_ATTR_MODULE, OPERAND1_1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, - [LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = { .nuops = 2, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_NONDESCRIPTOR_NO_DICT, 4, 5 } } }, - [LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = { .nuops = 4, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, OPARG_SIMPLE, 3 }, { _GUARD_KEYS_VERSION, 2, 3 }, { _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES, 4, 5 } } }, - [LOAD_ATTR_PROPERTY] = { .nuops = 5, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_PROPERTY_FRAME, 4, 5 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 9 }, { _PUSH_FRAME, OPARG_SIMPLE, 9 } } }, - [LOAD_ATTR_SLOT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_SLOT, 1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, - [LOAD_ATTR_WITH_HINT] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_WITH_HINT, 1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_CLASS] = { .nuops = 4, .uops = { { _RECORD_TOS, OPARG_SIMPLE, 1 }, { _CHECK_ATTR_CLASS, 2, 1 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { .nuops = 5, .uops = { { _RECORD_TOS, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_ATTR_CLASS, 2, 3 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN] = { .nuops = 7, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_PEP_523, OPARG_SIMPLE, 3 }, { _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME, 2, 3 }, { _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME, OPERAND1_4, 5 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 9 }, { _PUSH_FRAME, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_INSTANCE_VALUE] = { .nuops = 6, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_MANAGED_OBJECT_HAS_VALUES, OPARG_SIMPLE, 3 }, { _LOAD_ATTR_INSTANCE_VALUE, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_METHOD_LAZY_DICT] = { .nuops = 4, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_ATTR_METHOD_LAZY_DICT, 1, 3 }, { _LOAD_ATTR_METHOD_LAZY_DICT, 4, 5 } } }, + [LOAD_ATTR_METHOD_NO_DICT] = { .nuops = 3, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_METHOD_NO_DICT, 4, 5 } } }, + [LOAD_ATTR_METHOD_WITH_VALUES] = { .nuops = 5, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, OPARG_SIMPLE, 3 }, { _GUARD_KEYS_VERSION, 2, 3 }, { _LOAD_ATTR_METHOD_WITH_VALUES, 4, 5 } } }, + [LOAD_ATTR_MODULE] = { .nuops = 4, .uops = { { _LOAD_ATTR_MODULE, 2, 1 }, { _LOAD_ATTR_MODULE, OPERAND1_1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = { .nuops = 3, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_NONDESCRIPTOR_NO_DICT, 4, 5 } } }, + [LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = { .nuops = 5, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, OPARG_SIMPLE, 3 }, { _GUARD_KEYS_VERSION, 2, 3 }, { _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES, 4, 5 } } }, + [LOAD_ATTR_PROPERTY] = { .nuops = 7, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_PEP_523, OPARG_SIMPLE, 3 }, { _LOAD_ATTR_PROPERTY_FRAME, 2, 3 }, { _LOAD_ATTR_PROPERTY_FRAME, OPERAND1_4, 5 }, { _SAVE_RETURN_OFFSET, OPARG_SAVE_RETURN_OFFSET, 9 }, { _PUSH_FRAME, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_SLOT] = { .nuops = 5, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_SLOT, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, + [LOAD_ATTR_WITH_HINT] = { .nuops = 5, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _LOAD_ATTR_WITH_HINT, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, [LOAD_BUILD_CLASS] = { .nuops = 1, .uops = { { _LOAD_BUILD_CLASS, OPARG_SIMPLE, 0 } } }, [LOAD_COMMON_CONSTANT] = { .nuops = 1, .uops = { { _LOAD_COMMON_CONSTANT, OPARG_SIMPLE, 0 } } }, - [LOAD_CONST_IMMORTAL] = { .nuops = 1, .uops = { { _LOAD_CONST_IMMORTAL, OPARG_SIMPLE, 0 } } }, - [LOAD_CONST_MORTAL] = { .nuops = 1, .uops = { { _LOAD_CONST_MORTAL, OPARG_SIMPLE, 0 } } }, + [LOAD_CONST] = { .nuops = 1, .uops = { { _LOAD_CONST, OPARG_SIMPLE, 0 } } }, [LOAD_DEREF] = { .nuops = 1, .uops = { { _LOAD_DEREF, OPARG_SIMPLE, 0 } } }, [LOAD_FAST] = { .nuops = 1, .uops = { { _LOAD_FAST, OPARG_SIMPLE, 0 } } }, [LOAD_FAST_AND_CLEAR] = { .nuops = 1, .uops = { { _LOAD_FAST_AND_CLEAR, OPARG_SIMPLE, 0 } } }, @@ -1445,26 +1494,27 @@ _PyOpcode_macro_expansion[256] = { [LOAD_FAST_CHECK] = { .nuops = 1, .uops = { { _LOAD_FAST_CHECK, OPARG_SIMPLE, 0 } } }, [LOAD_FAST_LOAD_FAST] = { .nuops = 2, .uops = { { _LOAD_FAST, OPARG_TOP, 0 }, { _LOAD_FAST, OPARG_BOTTOM, 0 } } }, [LOAD_FROM_DICT_OR_DEREF] = { .nuops = 1, .uops = { { _LOAD_FROM_DICT_OR_DEREF, OPARG_SIMPLE, 0 } } }, + [LOAD_FROM_DICT_OR_GLOBALS] = { .nuops = 1, .uops = { { _LOAD_FROM_DICT_OR_GLOBALS, OPARG_SIMPLE, 0 } } }, [LOAD_GLOBAL] = { .nuops = 2, .uops = { { _LOAD_GLOBAL, OPARG_SIMPLE, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 3 } } }, [LOAD_GLOBAL_BUILTIN] = { .nuops = 4, .uops = { { _GUARD_GLOBALS_VERSION, 1, 1 }, { _LOAD_GLOBAL_BUILTINS, 1, 2 }, { _LOAD_GLOBAL_BUILTINS, OPERAND1_1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 4 } } }, [LOAD_GLOBAL_MODULE] = { .nuops = 4, .uops = { { _NOP, OPARG_SIMPLE, 1 }, { _LOAD_GLOBAL_MODULE, 1, 1 }, { _LOAD_GLOBAL_MODULE, OPERAND1_1, 3 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 4 } } }, [LOAD_LOCALS] = { .nuops = 1, .uops = { { _LOAD_LOCALS, OPARG_SIMPLE, 0 } } }, [LOAD_NAME] = { .nuops = 1, .uops = { { _LOAD_NAME, OPARG_SIMPLE, 0 } } }, [LOAD_SMALL_INT] = { .nuops = 1, .uops = { { _LOAD_SMALL_INT, OPARG_SIMPLE, 0 } } }, - [LOAD_SPECIAL] = { .nuops = 2, .uops = { { _INSERT_NULL, OPARG_SIMPLE, 0 }, { _LOAD_SPECIAL, OPARG_SIMPLE, 0 } } }, + [LOAD_SPECIAL] = { .nuops = 3, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 0 }, { _INSERT_NULL, OPARG_SIMPLE, 0 }, { _LOAD_SPECIAL, OPARG_SIMPLE, 0 } } }, [LOAD_SUPER_ATTR_ATTR] = { .nuops = 1, .uops = { { _LOAD_SUPER_ATTR_ATTR, OPARG_SIMPLE, 1 } } }, - [LOAD_SUPER_ATTR_METHOD] = { .nuops = 1, .uops = { { _LOAD_SUPER_ATTR_METHOD, OPARG_SIMPLE, 1 } } }, + [LOAD_SUPER_ATTR_METHOD] = { .nuops = 3, .uops = { { _RECORD_NOS, OPARG_SIMPLE, 0 }, { _GUARD_LOAD_SUPER_ATTR_METHOD, OPARG_SIMPLE, 1 }, { _LOAD_SUPER_ATTR_METHOD, OPARG_SIMPLE, 1 } } }, [MAKE_CELL] = { .nuops = 1, .uops = { { _MAKE_CELL, OPARG_SIMPLE, 0 } } }, - [MAKE_FUNCTION] = { .nuops = 1, .uops = { { _MAKE_FUNCTION, OPARG_SIMPLE, 0 } } }, + [MAKE_FUNCTION] = { .nuops = 2, .uops = { { _MAKE_FUNCTION, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, [MAP_ADD] = { .nuops = 1, .uops = { { _MAP_ADD, OPARG_SIMPLE, 0 } } }, - [MATCH_CLASS] = { .nuops = 1, .uops = { { _MATCH_CLASS, OPARG_SIMPLE, 0 } } }, + [MATCH_CLASS] = { .nuops = 4, .uops = { { _MATCH_CLASS, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, [MATCH_KEYS] = { .nuops = 1, .uops = { { _MATCH_KEYS, OPARG_SIMPLE, 0 } } }, [MATCH_MAPPING] = { .nuops = 1, .uops = { { _MATCH_MAPPING, OPARG_SIMPLE, 0 } } }, [MATCH_SEQUENCE] = { .nuops = 1, .uops = { { _MATCH_SEQUENCE, OPARG_SIMPLE, 0 } } }, [NOP] = { .nuops = 1, .uops = { { _NOP, OPARG_SIMPLE, 0 } } }, [NOT_TAKEN] = { .nuops = 1, .uops = { { _NOP, OPARG_SIMPLE, 0 } } }, [POP_EXCEPT] = { .nuops = 1, .uops = { { _POP_EXCEPT, OPARG_SIMPLE, 0 } } }, - [POP_ITER] = { .nuops = 1, .uops = { { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [POP_ITER] = { .nuops = 1, .uops = { { _POP_ITER, OPARG_SIMPLE, 0 } } }, [POP_JUMP_IF_FALSE] = { .nuops = 1, .uops = { { _POP_JUMP_IF_FALSE, OPARG_REPLACED, 1 } } }, [POP_JUMP_IF_NONE] = { .nuops = 2, .uops = { { _IS_NONE, OPARG_SIMPLE, 1 }, { _POP_JUMP_IF_TRUE, OPARG_REPLACED, 1 } } }, [POP_JUMP_IF_NOT_NONE] = { .nuops = 2, .uops = { { _IS_NONE, OPARG_SIMPLE, 1 }, { _POP_JUMP_IF_FALSE, OPARG_REPLACED, 1 } } }, @@ -1472,38 +1522,40 @@ _PyOpcode_macro_expansion[256] = { [POP_TOP] = { .nuops = 1, .uops = { { _POP_TOP, OPARG_SIMPLE, 0 } } }, [PUSH_EXC_INFO] = { .nuops = 1, .uops = { { _PUSH_EXC_INFO, OPARG_SIMPLE, 0 } } }, [PUSH_NULL] = { .nuops = 1, .uops = { { _PUSH_NULL, OPARG_SIMPLE, 0 } } }, - [RESUME_CHECK] = { .nuops = 1, .uops = { { _RESUME_CHECK, OPARG_SIMPLE, 0 } } }, + [RESUME_CHECK] = { .nuops = 1, .uops = { { _RESUME_CHECK, OPARG_SIMPLE, 1 } } }, [RETURN_GENERATOR] = { .nuops = 1, .uops = { { _RETURN_GENERATOR, OPARG_SIMPLE, 0 } } }, - [RETURN_VALUE] = { .nuops = 1, .uops = { { _RETURN_VALUE, OPARG_SIMPLE, 0 } } }, - [SEND_GEN] = { .nuops = 3, .uops = { { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _SEND_GEN_FRAME, OPARG_SIMPLE, 1 }, { _PUSH_FRAME, OPARG_SIMPLE, 1 } } }, + [RETURN_VALUE] = { .nuops = 2, .uops = { { _MAKE_HEAP_SAFE, OPARG_SIMPLE, 0 }, { _RETURN_VALUE, OPARG_SIMPLE, 0 } } }, + [SEND_ASYNC_GEN] = { .nuops = 2, .uops = { { _GUARD_3OS_ASYNC_GEN_ASEND, OPARG_SIMPLE, 1 }, { _SEND_ASYNC_GEN, OPARG_REPLACED, 1 } } }, + [SEND_GEN] = { .nuops = 4, .uops = { { _RECORD_3OS_GEN_FUNC, OPARG_SIMPLE, 1 }, { _CHECK_PEP_523, OPARG_SIMPLE, 1 }, { _SEND_GEN_FRAME, OPARG_SIMPLE, 1 }, { _PUSH_FRAME, OPARG_SIMPLE, 1 } } }, + [SEND_VIRTUAL] = { .nuops = 3, .uops = { { _GUARD_TOS_IS_NONE, OPARG_SIMPLE, 1 }, { _GUARD_NOS_NOT_NULL, OPARG_SIMPLE, 1 }, { _SEND_VIRTUAL, OPARG_REPLACED, 1 } } }, [SETUP_ANNOTATIONS] = { .nuops = 1, .uops = { { _SETUP_ANNOTATIONS, OPARG_SIMPLE, 0 } } }, [SET_ADD] = { .nuops = 1, .uops = { { _SET_ADD, OPARG_SIMPLE, 0 } } }, [SET_FUNCTION_ATTRIBUTE] = { .nuops = 1, .uops = { { _SET_FUNCTION_ATTRIBUTE, OPARG_SIMPLE, 0 } } }, - [SET_UPDATE] = { .nuops = 1, .uops = { { _SET_UPDATE, OPARG_SIMPLE, 0 } } }, + [SET_UPDATE] = { .nuops = 2, .uops = { { _SET_UPDATE, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, [STORE_ATTR] = { .nuops = 1, .uops = { { _STORE_ATTR, OPARG_SIMPLE, 3 } } }, - [STORE_ATTR_INSTANCE_VALUE] = { .nuops = 3, .uops = { { _GUARD_TYPE_VERSION_AND_LOCK, 2, 1 }, { _GUARD_DORV_NO_DICT, OPARG_SIMPLE, 3 }, { _STORE_ATTR_INSTANCE_VALUE, 1, 3 } } }, - [STORE_ATTR_SLOT] = { .nuops = 2, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _STORE_ATTR_SLOT, 1, 3 } } }, - [STORE_ATTR_WITH_HINT] = { .nuops = 2, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _STORE_ATTR_WITH_HINT, 1, 3 } } }, + [STORE_ATTR_INSTANCE_VALUE] = { .nuops = 6, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _LOCK_OBJECT, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION_LOCKED, 2, 1 }, { _GUARD_DORV_NO_DICT, OPARG_SIMPLE, 3 }, { _STORE_ATTR_INSTANCE_VALUE, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 } } }, + [STORE_ATTR_SLOT] = { .nuops = 4, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _STORE_ATTR_SLOT, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 } } }, + [STORE_ATTR_WITH_HINT] = { .nuops = 4, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _STORE_ATTR_WITH_HINT, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 } } }, [STORE_DEREF] = { .nuops = 1, .uops = { { _STORE_DEREF, OPARG_SIMPLE, 0 } } }, - [STORE_FAST] = { .nuops = 1, .uops = { { _STORE_FAST, OPARG_SIMPLE, 0 } } }, - [STORE_FAST_LOAD_FAST] = { .nuops = 2, .uops = { { _STORE_FAST, OPARG_TOP, 0 }, { _LOAD_FAST, OPARG_BOTTOM, 0 } } }, - [STORE_FAST_STORE_FAST] = { .nuops = 2, .uops = { { _STORE_FAST, OPARG_TOP, 0 }, { _STORE_FAST, OPARG_BOTTOM, 0 } } }, + [STORE_FAST] = { .nuops = 2, .uops = { { _SWAP_FAST, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [STORE_FAST_LOAD_FAST] = { .nuops = 3, .uops = { { _SWAP_FAST, OPARG_TOP, 0 }, { _POP_TOP, OPARG_TOP, 0 }, { _LOAD_FAST, OPARG_BOTTOM, 0 } } }, + [STORE_FAST_STORE_FAST] = { .nuops = 4, .uops = { { _SWAP_FAST, OPARG_TOP, 0 }, { _POP_TOP, OPARG_TOP, 0 }, { _SWAP_FAST, OPARG_BOTTOM, 0 }, { _POP_TOP, OPARG_BOTTOM, 0 } } }, [STORE_GLOBAL] = { .nuops = 1, .uops = { { _STORE_GLOBAL, OPARG_SIMPLE, 0 } } }, [STORE_NAME] = { .nuops = 1, .uops = { { _STORE_NAME, OPARG_SIMPLE, 0 } } }, [STORE_SLICE] = { .nuops = 1, .uops = { { _STORE_SLICE, OPARG_SIMPLE, 0 } } }, [STORE_SUBSCR] = { .nuops = 1, .uops = { { _STORE_SUBSCR, OPARG_SIMPLE, 0 } } }, - [STORE_SUBSCR_DICT] = { .nuops = 2, .uops = { { _GUARD_NOS_DICT, OPARG_SIMPLE, 0 }, { _STORE_SUBSCR_DICT, OPARG_SIMPLE, 1 } } }, - [STORE_SUBSCR_LIST_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _STORE_SUBSCR_LIST_INT, OPARG_SIMPLE, 1 } } }, + [STORE_SUBSCR_DICT] = { .nuops = 4, .uops = { { _RECORD_NOS_TYPE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_DICT_STORE_SUBSCRIPT, OPARG_SIMPLE, 0 }, { _STORE_SUBSCR_DICT, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 } } }, + [STORE_SUBSCR_LIST_INT] = { .nuops = 5, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_LIST, OPARG_SIMPLE, 0 }, { _STORE_SUBSCR_LIST_INT, OPARG_SIMPLE, 1 }, { _POP_TOP_INT, OPARG_SIMPLE, 1 }, { _POP_TOP, OPARG_SIMPLE, 1 } } }, [SWAP] = { .nuops = 1, .uops = { { _SWAP, OPARG_SIMPLE, 0 } } }, [TO_BOOL] = { .nuops = 1, .uops = { { _TO_BOOL, OPARG_SIMPLE, 2 } } }, - [TO_BOOL_ALWAYS_TRUE] = { .nuops = 2, .uops = { { _GUARD_TYPE_VERSION, 2, 1 }, { _REPLACE_WITH_TRUE, OPARG_SIMPLE, 3 } } }, + [TO_BOOL_ALWAYS_TRUE] = { .nuops = 4, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _REPLACE_WITH_TRUE, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 } } }, [TO_BOOL_BOOL] = { .nuops = 1, .uops = { { _TO_BOOL_BOOL, OPARG_SIMPLE, 3 } } }, - [TO_BOOL_INT] = { .nuops = 1, .uops = { { _TO_BOOL_INT, OPARG_SIMPLE, 3 } } }, - [TO_BOOL_LIST] = { .nuops = 2, .uops = { { _GUARD_TOS_LIST, OPARG_SIMPLE, 0 }, { _TO_BOOL_LIST, OPARG_SIMPLE, 3 } } }, + [TO_BOOL_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _TO_BOOL_INT, OPARG_SIMPLE, 3 }, { _POP_TOP_INT, OPARG_SIMPLE, 3 } } }, + [TO_BOOL_LIST] = { .nuops = 3, .uops = { { _GUARD_TOS_LIST, OPARG_SIMPLE, 0 }, { _TO_BOOL_LIST, OPARG_SIMPLE, 3 }, { _POP_TOP, OPARG_SIMPLE, 3 } } }, [TO_BOOL_NONE] = { .nuops = 1, .uops = { { _TO_BOOL_NONE, OPARG_SIMPLE, 3 } } }, - [TO_BOOL_STR] = { .nuops = 2, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _TO_BOOL_STR, OPARG_SIMPLE, 3 } } }, - [UNARY_INVERT] = { .nuops = 1, .uops = { { _UNARY_INVERT, OPARG_SIMPLE, 0 } } }, - [UNARY_NEGATIVE] = { .nuops = 1, .uops = { { _UNARY_NEGATIVE, OPARG_SIMPLE, 0 } } }, + [TO_BOOL_STR] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _TO_BOOL_STR, OPARG_SIMPLE, 3 }, { _POP_TOP_UNICODE, OPARG_SIMPLE, 3 } } }, + [UNARY_INVERT] = { .nuops = 2, .uops = { { _UNARY_INVERT, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, + [UNARY_NEGATIVE] = { .nuops = 2, .uops = { { _UNARY_NEGATIVE, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, [UNARY_NOT] = { .nuops = 1, .uops = { { _UNARY_NOT, OPARG_SIMPLE, 0 } } }, [UNPACK_EX] = { .nuops = 1, .uops = { { _UNPACK_EX, OPARG_SIMPLE, 0 } } }, [UNPACK_SEQUENCE] = { .nuops = 1, .uops = { { _UNPACK_SEQUENCE, OPARG_SIMPLE, 0 } } }, @@ -1511,11 +1563,11 @@ _PyOpcode_macro_expansion[256] = { [UNPACK_SEQUENCE_TUPLE] = { .nuops = 2, .uops = { { _GUARD_TOS_TUPLE, OPARG_SIMPLE, 0 }, { _UNPACK_SEQUENCE_TUPLE, OPARG_SIMPLE, 1 } } }, [UNPACK_SEQUENCE_TWO_TUPLE] = { .nuops = 2, .uops = { { _GUARD_TOS_TUPLE, OPARG_SIMPLE, 0 }, { _UNPACK_SEQUENCE_TWO_TUPLE, OPARG_SIMPLE, 1 } } }, [WITH_EXCEPT_START] = { .nuops = 1, .uops = { { _WITH_EXCEPT_START, OPARG_SIMPLE, 0 } } }, - [YIELD_VALUE] = { .nuops = 1, .uops = { { _YIELD_VALUE, OPARG_SIMPLE, 0 } } }, + [YIELD_VALUE] = { .nuops = 2, .uops = { { _MAKE_HEAP_SAFE, OPARG_SIMPLE, 0 }, { _YIELD_VALUE, OPARG_SIMPLE, 0 } } }, }; #endif // NEED_OPCODE_METADATA -extern const char *_PyOpcode_OpName[267]; +PyAPI_DATA(const char) *_PyOpcode_OpName[267]; #ifdef NEED_OPCODE_METADATA const char *_PyOpcode_OpName[267] = { [ANNOTATIONS_PLACEHOLDER] = "ANNOTATIONS_PLACEHOLDER", @@ -1533,6 +1585,7 @@ const char *_PyOpcode_OpName[267] = { [BINARY_OP_SUBSCR_LIST_SLICE] = "BINARY_OP_SUBSCR_LIST_SLICE", [BINARY_OP_SUBSCR_STR_INT] = "BINARY_OP_SUBSCR_STR_INT", [BINARY_OP_SUBSCR_TUPLE_INT] = "BINARY_OP_SUBSCR_TUPLE_INT", + [BINARY_OP_SUBSCR_USTR_INT] = "BINARY_OP_SUBSCR_USTR_INT", [BINARY_OP_SUBTRACT_FLOAT] = "BINARY_OP_SUBTRACT_FLOAT", [BINARY_OP_SUBTRACT_INT] = "BINARY_OP_SUBTRACT_INT", [BINARY_SLICE] = "BINARY_SLICE", @@ -1553,6 +1606,8 @@ const char *_PyOpcode_OpName[267] = { [CALL_BUILTIN_FAST] = "CALL_BUILTIN_FAST", [CALL_BUILTIN_FAST_WITH_KEYWORDS] = "CALL_BUILTIN_FAST_WITH_KEYWORDS", [CALL_BUILTIN_O] = "CALL_BUILTIN_O", + [CALL_EX_NON_PY_GENERAL] = "CALL_EX_NON_PY_GENERAL", + [CALL_EX_PY] = "CALL_EX_PY", [CALL_FUNCTION_EX] = "CALL_FUNCTION_EX", [CALL_INTRINSIC_1] = "CALL_INTRINSIC_1", [CALL_INTRINSIC_2] = "CALL_INTRINSIC_2", @@ -1607,12 +1662,14 @@ const char *_PyOpcode_OpName[267] = { [FOR_ITER_LIST] = "FOR_ITER_LIST", [FOR_ITER_RANGE] = "FOR_ITER_RANGE", [FOR_ITER_TUPLE] = "FOR_ITER_TUPLE", + [FOR_ITER_VIRTUAL] = "FOR_ITER_VIRTUAL", [GET_AITER] = "GET_AITER", [GET_ANEXT] = "GET_ANEXT", [GET_AWAITABLE] = "GET_AWAITABLE", [GET_ITER] = "GET_ITER", + [GET_ITER_SELF] = "GET_ITER_SELF", + [GET_ITER_VIRTUAL] = "GET_ITER_VIRTUAL", [GET_LEN] = "GET_LEN", - [GET_YIELD_FROM_ITER] = "GET_YIELD_FROM_ITER", [IMPORT_FROM] = "IMPORT_FROM", [IMPORT_NAME] = "IMPORT_NAME", [INSTRUMENTED_CALL] = "INSTRUMENTED_CALL", @@ -1667,8 +1724,6 @@ const char *_PyOpcode_OpName[267] = { [LOAD_CLOSURE] = "LOAD_CLOSURE", [LOAD_COMMON_CONSTANT] = "LOAD_COMMON_CONSTANT", [LOAD_CONST] = "LOAD_CONST", - [LOAD_CONST_IMMORTAL] = "LOAD_CONST_IMMORTAL", - [LOAD_CONST_MORTAL] = "LOAD_CONST_MORTAL", [LOAD_DEREF] = "LOAD_DEREF", [LOAD_FAST] = "LOAD_FAST", [LOAD_FAST_AND_CLEAR] = "LOAD_FAST_AND_CLEAR", @@ -1712,10 +1767,13 @@ const char *_PyOpcode_OpName[267] = { [RESERVED] = "RESERVED", [RESUME] = "RESUME", [RESUME_CHECK] = "RESUME_CHECK", + [RESUME_CHECK_JIT] = "RESUME_CHECK_JIT", [RETURN_GENERATOR] = "RETURN_GENERATOR", [RETURN_VALUE] = "RETURN_VALUE", [SEND] = "SEND", + [SEND_ASYNC_GEN] = "SEND_ASYNC_GEN", [SEND_GEN] = "SEND_GEN", + [SEND_VIRTUAL] = "SEND_VIRTUAL", [SETUP_ANNOTATIONS] = "SETUP_ANNOTATIONS", [SETUP_CLEANUP] = "SETUP_CLEANUP", [SETUP_FINALLY] = "SETUP_FINALLY", @@ -1746,6 +1804,7 @@ const char *_PyOpcode_OpName[267] = { [TO_BOOL_LIST] = "TO_BOOL_LIST", [TO_BOOL_NONE] = "TO_BOOL_NONE", [TO_BOOL_STR] = "TO_BOOL_STR", + [TRACE_RECORD] = "TRACE_RECORD", [UNARY_INVERT] = "UNARY_INVERT", [UNARY_NEGATIVE] = "UNARY_NEGATIVE", [UNARY_NOT] = "UNARY_NOT", @@ -1759,12 +1818,14 @@ const char *_PyOpcode_OpName[267] = { }; #endif -extern const uint8_t _PyOpcode_Caches[256]; +PyAPI_DATA(const uint8_t) _PyOpcode_Caches[256]; #ifdef NEED_OPCODE_METADATA const uint8_t _PyOpcode_Caches[256] = { + [RESUME] = 1, [TO_BOOL] = 3, [STORE_SUBSCR] = 1, [SEND] = 1, + [FOR_ITER] = 1, [UNPACK_SEQUENCE] = 1, [STORE_ATTR] = 4, [LOAD_GLOBAL] = 4, @@ -1777,16 +1838,18 @@ const uint8_t _PyOpcode_Caches[256] = { [POP_JUMP_IF_FALSE] = 1, [POP_JUMP_IF_NONE] = 1, [POP_JUMP_IF_NOT_NONE] = 1, - [FOR_ITER] = 1, + [GET_ITER] = 1, [CALL] = 3, [CALL_KW] = 3, + [CALL_FUNCTION_EX] = 1, [BINARY_OP] = 5, }; #endif -extern const uint8_t _PyOpcode_Deopt[256]; +PyAPI_DATA(const uint8_t) _PyOpcode_Deopt[256]; #ifdef NEED_OPCODE_METADATA const uint8_t _PyOpcode_Deopt[256] = { + [120] = 120, [121] = 121, [122] = 122, [123] = 123, @@ -1794,13 +1857,6 @@ const uint8_t _PyOpcode_Deopt[256] = { [125] = 125, [126] = 126, [127] = 127, - [212] = 212, - [213] = 213, - [214] = 214, - [215] = 215, - [216] = 216, - [217] = 217, - [218] = 218, [219] = 219, [220] = 220, [221] = 221, @@ -1815,7 +1871,6 @@ const uint8_t _PyOpcode_Deopt[256] = { [230] = 230, [231] = 231, [232] = 232, - [233] = 233, [BINARY_OP] = BINARY_OP, [BINARY_OP_ADD_FLOAT] = BINARY_OP, [BINARY_OP_ADD_INT] = BINARY_OP, @@ -1830,6 +1885,7 @@ const uint8_t _PyOpcode_Deopt[256] = { [BINARY_OP_SUBSCR_LIST_SLICE] = BINARY_OP, [BINARY_OP_SUBSCR_STR_INT] = BINARY_OP, [BINARY_OP_SUBSCR_TUPLE_INT] = BINARY_OP, + [BINARY_OP_SUBSCR_USTR_INT] = BINARY_OP, [BINARY_OP_SUBTRACT_FLOAT] = BINARY_OP, [BINARY_OP_SUBTRACT_INT] = BINARY_OP, [BINARY_SLICE] = BINARY_SLICE, @@ -1850,6 +1906,8 @@ const uint8_t _PyOpcode_Deopt[256] = { [CALL_BUILTIN_FAST] = CALL, [CALL_BUILTIN_FAST_WITH_KEYWORDS] = CALL, [CALL_BUILTIN_O] = CALL, + [CALL_EX_NON_PY_GENERAL] = CALL_FUNCTION_EX, + [CALL_EX_PY] = CALL_FUNCTION_EX, [CALL_FUNCTION_EX] = CALL_FUNCTION_EX, [CALL_INTRINSIC_1] = CALL_INTRINSIC_1, [CALL_INTRINSIC_2] = CALL_INTRINSIC_2, @@ -1904,12 +1962,14 @@ const uint8_t _PyOpcode_Deopt[256] = { [FOR_ITER_LIST] = FOR_ITER, [FOR_ITER_RANGE] = FOR_ITER, [FOR_ITER_TUPLE] = FOR_ITER, + [FOR_ITER_VIRTUAL] = FOR_ITER, [GET_AITER] = GET_AITER, [GET_ANEXT] = GET_ANEXT, [GET_AWAITABLE] = GET_AWAITABLE, [GET_ITER] = GET_ITER, + [GET_ITER_SELF] = GET_ITER, + [GET_ITER_VIRTUAL] = GET_ITER, [GET_LEN] = GET_LEN, - [GET_YIELD_FROM_ITER] = GET_YIELD_FROM_ITER, [IMPORT_FROM] = IMPORT_FROM, [IMPORT_NAME] = IMPORT_NAME, [INSTRUMENTED_CALL] = INSTRUMENTED_CALL, @@ -1959,8 +2019,6 @@ const uint8_t _PyOpcode_Deopt[256] = { [LOAD_BUILD_CLASS] = LOAD_BUILD_CLASS, [LOAD_COMMON_CONSTANT] = LOAD_COMMON_CONSTANT, [LOAD_CONST] = LOAD_CONST, - [LOAD_CONST_IMMORTAL] = LOAD_CONST, - [LOAD_CONST_MORTAL] = LOAD_CONST, [LOAD_DEREF] = LOAD_DEREF, [LOAD_FAST] = LOAD_FAST, [LOAD_FAST_AND_CLEAR] = LOAD_FAST_AND_CLEAR, @@ -2003,10 +2061,13 @@ const uint8_t _PyOpcode_Deopt[256] = { [RESERVED] = RESERVED, [RESUME] = RESUME, [RESUME_CHECK] = RESUME, + [RESUME_CHECK_JIT] = RESUME, [RETURN_GENERATOR] = RETURN_GENERATOR, [RETURN_VALUE] = RETURN_VALUE, [SEND] = SEND, + [SEND_ASYNC_GEN] = SEND, [SEND_GEN] = SEND, + [SEND_VIRTUAL] = SEND, [SETUP_ANNOTATIONS] = SETUP_ANNOTATIONS, [SET_ADD] = SET_ADD, [SET_FUNCTION_ATTRIBUTE] = SET_FUNCTION_ATTRIBUTE, @@ -2033,6 +2094,7 @@ const uint8_t _PyOpcode_Deopt[256] = { [TO_BOOL_LIST] = TO_BOOL, [TO_BOOL_NONE] = TO_BOOL, [TO_BOOL_STR] = TO_BOOL, + [TRACE_RECORD] = TRACE_RECORD, [UNARY_INVERT] = UNARY_INVERT, [UNARY_NEGATIVE] = UNARY_NEGATIVE, [UNARY_NOT] = UNARY_NOT, @@ -2048,6 +2110,7 @@ const uint8_t _PyOpcode_Deopt[256] = { #endif // NEED_OPCODE_METADATA #define EXTRA_CASES \ + case 120: \ case 121: \ case 122: \ case 123: \ @@ -2055,13 +2118,6 @@ const uint8_t _PyOpcode_Deopt[256] = { case 125: \ case 126: \ case 127: \ - case 212: \ - case 213: \ - case 214: \ - case 215: \ - case 216: \ - case 217: \ - case 218: \ case 219: \ case 220: \ case 221: \ @@ -2076,7 +2132,6 @@ const uint8_t _PyOpcode_Deopt[256] = { case 230: \ case 231: \ case 232: \ - case 233: \ ; struct pseudo_targets { uint8_t as_sequence; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_utils.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_utils.h index 79a1a2425..3e2c4ae41 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_utils.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_opcode_utils.h @@ -73,16 +73,28 @@ extern "C" { #define CONSTANT_BUILTIN_TUPLE 2 #define CONSTANT_BUILTIN_ALL 3 #define CONSTANT_BUILTIN_ANY 4 -#define NUM_COMMON_CONSTANTS 5 +#define CONSTANT_BUILTIN_LIST 5 +#define CONSTANT_BUILTIN_SET 6 +#define CONSTANT_NONE 7 +#define CONSTANT_EMPTY_STR 8 +#define CONSTANT_TRUE 9 +#define CONSTANT_FALSE 10 +#define CONSTANT_MINUS_ONE 11 +#define NUM_COMMON_CONSTANTS 12 /* Values used in the oparg for RESUME */ #define RESUME_AT_FUNC_START 0 #define RESUME_AFTER_YIELD 1 #define RESUME_AFTER_YIELD_FROM 2 #define RESUME_AFTER_AWAIT 3 +#define RESUME_AT_GEN_EXPR_START 4 -#define RESUME_OPARG_LOCATION_MASK 0x3 -#define RESUME_OPARG_DEPTH1_MASK 0x4 +#define RESUME_OPARG_LOCATION_MASK 0x7 +#define RESUME_OPARG_DEPTH1_MASK 0x8 + +#define GET_ITER_YIELD_FROM 1 +#define GET_ITER_YIELD_FROM_NO_CHECK 2 +#define GET_ITER_YIELD_FROM_CORO_CHECK 3 #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_optimizer.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_optimizer.h index d36747269..25e04171f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_optimizer.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_optimizer.h @@ -9,65 +9,183 @@ extern "C" { #endif #include "pycore_typedefs.h" // _PyInterpreterFrame +#include "pycore_jit_publish.h" +#include "pycore_uop.h" // _PyUOpInstruction #include "pycore_uop_ids.h" +#include "pycore_stackref.h" // _PyStackRef +#include "pycore_optimizer_types.h" #include +/* Fitness controls how long a trace can grow. + * Starts at FITNESS_INITIAL, then decreases from per-bytecode buffer usage + * plus branch/frame heuristics. The trace stops when fitness drops below the + * current exit_quality. + * + * Design targets for the constants below: + * 1. Reaching the abstract frame-depth limit should drop fitness below + * EXIT_QUALITY_SPECIALIZABLE. + * 2. A backward edge should leave budget for roughly N_BACKWARD_SLACK more + * bytecodes, assuming AVG_SLOTS_PER_INSTRUCTION. + * 3. Roughly seven balanced branches should reduce fitness to + * EXIT_QUALITY_DEFAULT after per-slot costs. + * 4. A push followed by a matching return is net-zero on frame-specific + * fitness, excluding per-slot costs. + */ +#define OPTIMIZER_EFFECTIVENESS 2 +#define MAX_TARGET_LENGTH (FITNESS_INITIAL / OPTIMIZER_EFFECTIVENESS) + +/* Exit quality thresholds: trace stops when fitness < exit_quality. + * Higher = trace is more willing to stop here. */ +#define EXIT_QUALITY_CLOSE_LOOP (FITNESS_INITIAL - AVG_SLOTS_PER_INSTRUCTION*4) +#define EXIT_QUALITY_ENTER_EXECUTOR (FITNESS_INITIAL * 1 / 8) +#define EXIT_QUALITY_DEFAULT (FITNESS_INITIAL / 40) +#define EXIT_QUALITY_SPECIALIZABLE (FITNESS_INITIAL / 80) + +/* Estimated buffer slots per bytecode, used only to derive heuristics. + * Runtime charging uses trace-buffer capacity consumed for each bytecode. */ +#define AVG_SLOTS_PER_INSTRUCTION 6 + +/* Heuristic backward-edge exit quality: leave room for about 1 unroll and + * N_BACKWARD_SLACK more bytecodes before reaching EXIT_QUALITY_CLOSE_LOOP, + * based on AVG_SLOTS_PER_INSTRUCTION. */ +#define N_BACKWARD_SLACK 10 +#define EXIT_QUALITY_BACKWARD_EDGE (EXIT_QUALITY_CLOSE_LOOP / 2 - N_BACKWARD_SLACK * AVG_SLOTS_PER_INSTRUCTION) + +/* Penalty for a balanced branch. + * It is sized so repeated balanced branches can drive a trace toward + * EXIT_QUALITY_DEFAULT, while compute_branch_penalty() keeps any single branch + * from dominating the budget. + */ +#define FITNESS_BRANCH_BALANCED ((FITNESS_INITIAL - EXIT_QUALITY_DEFAULT - \ + (MAX_TARGET_LENGTH / 14 * AVG_SLOTS_PER_INSTRUCTION)) / (14)) + + +typedef struct _PyJitUopBuffer { + _PyUOpInstruction *start; + _PyUOpInstruction *next; + _PyUOpInstruction *end; +} _PyJitUopBuffer; + +typedef struct _JitOptRefBuffer { + JitOptRef *used; + JitOptRef *end; +} _JitOptRefBuffer; + +typedef struct _JitOptContext { + char done; + char out_of_space; + bool contradiction; + // Has the builtins dict been watched? + bool builtins_watched; + // The current "executing" frame. + _Py_UOpsAbstractFrame *frame; + _Py_UOpsAbstractFrame frames[MAX_ABSTRACT_FRAME_DEPTH]; + int curr_frame_depth; + + // Arena for the symbolic types. + ty_arena t_arena; + + /* To do -- We could make this more space efficient + * by using a single array and growing the stack and + * locals toward each other. */ + _JitOptRefBuffer locals; + _JitOptRefBuffer stack; + JitOptRef locals_array[ABSTRACT_INTERP_LOCALS_SIZE]; + JitOptRef stack_array[ABSTRACT_INTERP_STACK_SIZE]; + _PyJitUopBuffer out_buffer; + _PyBloomFilter *dependencies; +} JitOptContext; + + +static inline void +uop_buffer_init(_PyJitUopBuffer *trace, _PyUOpInstruction *start, uint32_t size) +{ + trace->next = trace->start = start; + trace->end = start + size; +} + +static inline _PyUOpInstruction * +uop_buffer_last(_PyJitUopBuffer *trace) +{ + assert(trace->next > trace->start); + return trace->next-1; +} + +static inline int +uop_buffer_length(_PyJitUopBuffer *trace) +{ + return (int)(trace->next - trace->start); +} + +static inline int +uop_buffer_remaining_space(_PyJitUopBuffer *trace) +{ + return (int)(trace->end - trace->next); +} + +typedef struct _PyJitTracerInitialState { + int stack_depth; + int chain_depth; + struct _PyExitData *exit; + PyCodeObject *code; // Strong + PyFunctionObject *func; // Strong + struct _PyExecutorObject *executor; // Strong + _Py_CODEUNIT *start_instr; + _Py_CODEUNIT *close_loop_instr; + _Py_CODEUNIT *jump_backward_instr; +} _PyJitTracerInitialState; + +#define MAX_RECORDED_VALUES 3 +typedef struct _PyJitTracerPreviousState { + int instr_oparg; + int instr_stacklevel; + _Py_CODEUNIT *instr; + PyCodeObject *instr_code; // Strong + struct _PyInterpreterFrame *instr_frame; + PyObject *recorded_values[MAX_RECORDED_VALUES]; // Strong, may be NULL + int recorded_count; +} _PyJitTracerPreviousState; + +typedef struct _PyJitTracerTranslatorState { + int32_t fitness; // Current trace fitness, starts high, decrements + int frame_depth; // Current inline depth (0 = root frame) +} _PyJitTracerTranslatorState; + +typedef struct _PyJitTracerState { + bool is_tracing; + _PyJitTracerInitialState initial_state; + _PyJitTracerPreviousState prev_state; + _PyJitTracerTranslatorState translator_state; + JitOptContext opt_context; + _PyJitUopBuffer code_buffer; + _PyJitUopBuffer out_buffer; + _PyUOpInstruction uop_array[2 * UOP_MAX_TRACE_LENGTH]; +} _PyJitTracerState; typedef struct _PyExecutorLinkListNode { struct _PyExecutorObject *next; struct _PyExecutorObject *previous; } _PyExecutorLinkListNode; - -/* Bloom filter with m = 256 - * https://en.wikipedia.org/wiki/Bloom_filter */ -#define _Py_BLOOM_FILTER_WORDS 8 - -typedef struct { - uint32_t bits[_Py_BLOOM_FILTER_WORDS]; -} _PyBloomFilter; - typedef struct { uint8_t opcode; uint8_t oparg; - uint8_t valid:1; - uint8_t linked:1; - uint8_t chain_depth:6; // Must be big enough for MAX_CHAIN_DEPTH - 1. - bool warm; - int index; // Index of ENTER_EXECUTOR (if code isn't NULL, below). - _PyBloomFilter bloom; - _PyExecutorLinkListNode links; + uint8_t valid; + uint8_t chain_depth; // Must be big enough for MAX_CHAIN_DEPTH - 1. + bool cold; + uint8_t pending_deletion; + int32_t index; // Index of ENTER_EXECUTOR (if code isn't NULL, below). + int32_t bloom_array_idx; // Index in interp->executor_blooms/executor_ptrs. + _PyExecutorLinkListNode links; // Used by deletion list. PyCodeObject *code; // Weak (NULL if no corresponding ENTER_EXECUTOR). } _PyVMData; -/* Depending on the format, - * the 32 bits between the oparg and operand are: - * UOP_FORMAT_TARGET: - * uint32_t target; - * UOP_FORMAT_JUMP - * uint16_t jump_target; - * uint16_t error_target; - */ -typedef struct { - uint16_t opcode:15; - uint16_t format:1; - uint16_t oparg; - union { - uint32_t target; - struct { - uint16_t jump_target; - uint16_t error_target; - }; - }; - uint64_t operand0; // A cache entry - uint64_t operand1; -#ifdef Py_STATS - uint64_t execution_count; -#endif -} _PyUOpInstruction; - -typedef struct { +typedef struct _PyExitData { uint32_t target; + uint16_t index:12; + uint16_t stack_cache:2; + uint16_t is_dynamic:1; + uint16_t is_control_flow:1; _Py_BackoffCounter temperature; struct _PyExecutorObject *executor; } _PyExitData; @@ -80,24 +198,96 @@ typedef struct _PyExecutorObject { uint32_t code_size; size_t jit_size; void *jit_code; - void *jit_side_entry; + _PyJitCodeRegistration *jit_registration; _PyExitData exits[1]; } _PyExecutorObject; -/* If pending deletion list gets large enough, then scan, - * and free any executors that aren't executing - * i.e. any that aren't a thread's current_executor. */ -#define EXECUTOR_DELETE_LIST_MAX 100 - // Export for '_opcode' shared extension (JIT compiler). PyAPI_FUNC(_PyExecutorObject*) _Py_GetExecutor(PyCodeObject *code, int offset); -void _Py_ExecutorInit(_PyExecutorObject *, const _PyBloomFilter *); -void _Py_ExecutorDetach(_PyExecutorObject *); -void _Py_BloomFilter_Init(_PyBloomFilter *); -void _Py_BloomFilter_Add(_PyBloomFilter *bloom, void *obj); +int _Py_ExecutorInit(_PyExecutorObject *, const _PyBloomFilter *); +PyAPI_FUNC(void) _Py_ExecutorDetach(_PyExecutorObject *); PyAPI_FUNC(void) _Py_Executor_DependsOn(_PyExecutorObject *executor, void *obj); +/* We use a bloomfilter with k = 6, m = 256 + * The choice of k and the following constants + * could do with a more rigorous analysis, + * but here is a simple analysis: + * + * We want to keep the false positive rate low. + * For n = 5 (a trace depends on 5 objects), + * we expect 30 bits set, giving a false positive + * rate of (30/256)**6 == 2.5e-6 which is plenty + * good enough. + * + * However with n = 10 we expect 60 bits set (worst case), + * giving a false positive of (60/256)**6 == 0.0001 + * + * We choose k = 6, rather than a higher number as + * it means the false positive rate grows slower for high n. + * + * n = 5, k = 6 => fp = 2.6e-6 + * n = 5, k = 8 => fp = 3.5e-7 + * n = 10, k = 6 => fp = 1.6e-4 + * n = 10, k = 8 => fp = 0.9e-4 + * n = 15, k = 6 => fp = 0.18% + * n = 15, k = 8 => fp = 0.23% + * n = 20, k = 6 => fp = 1.1% + * n = 20, k = 8 => fp = 2.3% + * + * The above analysis assumes perfect hash functions, + * but those don't exist, so the real false positive + * rates may be worse. + */ + +#define _Py_BLOOM_FILTER_K 6 +#define _Py_BLOOM_FILTER_SEED 20221211 + +static inline uint64_t +address_to_hash(void *ptr) { + assert(ptr != NULL); + uint64_t uhash = _Py_BLOOM_FILTER_SEED; + uintptr_t addr = (uintptr_t)ptr; + for (int i = 0; i < SIZEOF_VOID_P; i++) { + uhash ^= addr & 255; + uhash *= (uint64_t)PyHASH_MULTIPLIER; + addr >>= 8; + } + return uhash; +} + +static inline void +_Py_BloomFilter_Init(_PyBloomFilter *bloom) +{ + for (int i = 0; i < _Py_BLOOM_FILTER_WORDS; i++) { + bloom->bits[i] = 0; + } +} + +static inline void +_Py_BloomFilter_Add(_PyBloomFilter *bloom, void *ptr) +{ + uint64_t hash = address_to_hash(ptr); + assert(_Py_BLOOM_FILTER_K <= 8); + for (int i = 0; i < _Py_BLOOM_FILTER_K; i++) { + uint8_t bits = hash & 255; + bloom->bits[bits >> _Py_BLOOM_FILTER_WORD_SHIFT] |= + (_Py_bloom_filter_word_t)1 << (bits & (_Py_BLOOM_FILTER_BITS_PER_WORD - 1)); + hash >>= 8; + } +} + +static inline bool +bloom_filter_may_contain(const _PyBloomFilter *bloom, const _PyBloomFilter *hashes) +{ + for (int i = 0; i < _Py_BLOOM_FILTER_WORDS; i++) { + if ((bloom->bits[i] & hashes->bits[i]) != hashes->bits[i]) { + return false; + } + } + return true; +} + #define _Py_MAX_ALLOWED_BUILTINS_MODIFICATIONS 3 #define _Py_MAX_ALLOWED_GLOBALS_MODIFICATIONS 6 @@ -109,22 +299,18 @@ PyAPI_FUNC(void) _Py_Executors_InvalidateCold(PyInterpreterState *interp); #else # define _Py_Executors_InvalidateDependency(A, B, C) ((void)0) # define _Py_Executors_InvalidateAll(A, B) ((void)0) -# define _Py_Executors_InvalidateCold(A) ((void)0) #endif // Used as the threshold to trigger executor invalidation when -// trace_run_counter is greater than this value. -#define JIT_CLEANUP_THRESHOLD 100000 - -// This is the length of the trace we project initially. -#define UOP_MAX_TRACE_LENGTH 800 +// executor_creation_counter is greater than this value. +// This value is arbitrary and was not optimized. +#define JIT_CLEANUP_THRESHOLD 1000 -#define TRACE_STACK_SIZE 5 - -int _Py_uop_analyze_and_optimize(_PyInterpreterFrame *frame, - _PyUOpInstruction *trace, int trace_len, int curr_stackentries, - _PyBloomFilter *dependencies); +int _Py_uop_analyze_and_optimize( + _PyThreadStateImpl *tstate, + _PyUOpInstruction *input, int trace_len, int curr_stackentries, + _PyUOpInstruction *output, _PyBloomFilter *dependencies); extern PyTypeObject _PyUOpExecutor_Type; @@ -150,166 +336,220 @@ static inline uint16_t uop_get_error_target(const _PyUOpInstruction *inst) return inst->error_target; } -// Holds locals, stack, locals, stack ... co_consts (in that order) -#define MAX_ABSTRACT_INTERP_SIZE 4096 - -#define TY_ARENA_SIZE (UOP_MAX_TRACE_LENGTH * 5) - -// Need extras for root frame and for overflow frame (see TRACE_STACK_PUSH()) -#define MAX_ABSTRACT_FRAME_DEPTH (TRACE_STACK_SIZE + 2) -// The maximum number of side exits that we can take before requiring forward -// progress (and inserting a new ENTER_EXECUTOR instruction). In practice, this -// is the "maximum amount of polymorphism" that an isolated trace tree can -// handle before rejoining the rest of the program. -#define MAX_CHAIN_DEPTH 4 +#define REF_IS_BORROWED 1 +#define REF_IS_UNIQUE 2 +#define REF_IS_INVALID 3 +#define REF_TAG_BITS 3 -/* Symbols */ -/* See explanation in optimizer_symbols.c */ +#define REF_GET_TAG(x) ((uintptr_t)(x) & (REF_TAG_BITS)) +#define REF_CLEAR_TAG(x) ((uintptr_t)(x) & (~REF_TAG_BITS)) +#define JIT_BITS_TO_PTR_MASKED(REF) ((JitOptSymbol *)(((REF).bits) & (~REF_TAG_BITS))) -typedef enum _JitSymType { - JIT_SYM_UNKNOWN_TAG = 1, - JIT_SYM_NULL_TAG = 2, - JIT_SYM_NON_NULL_TAG = 3, - JIT_SYM_BOTTOM_TAG = 4, - JIT_SYM_TYPE_VERSION_TAG = 5, - JIT_SYM_KNOWN_CLASS_TAG = 6, - JIT_SYM_KNOWN_VALUE_TAG = 7, - JIT_SYM_TUPLE_TAG = 8, - JIT_SYM_TRUTHINESS_TAG = 9, -} JitSymType; - -typedef struct _jit_opt_known_class { - uint8_t tag; - uint32_t version; - PyTypeObject *type; -} JitOptKnownClass; - -typedef struct _jit_opt_known_version { - uint8_t tag; - uint32_t version; -} JitOptKnownVersion; +static inline JitOptSymbol * +PyJitRef_Unwrap(JitOptRef ref) +{ + return JIT_BITS_TO_PTR_MASKED(ref); +} -typedef struct _jit_opt_known_value { - uint8_t tag; - PyObject *value; -} JitOptKnownValue; +bool _Py_uop_symbol_is_immortal(JitOptSymbol *sym); -#define MAX_SYMBOLIC_TUPLE_SIZE 7 -typedef struct _jit_opt_tuple { - uint8_t tag; - uint8_t length; - uint16_t items[MAX_SYMBOLIC_TUPLE_SIZE]; -} JitOptTuple; +static inline JitOptRef +PyJitRef_Wrap(JitOptSymbol *sym) +{ + return (JitOptRef){.bits=(uintptr_t)sym}; +} -typedef struct { - uint8_t tag; - bool invert; - uint16_t value; -} JitOptTruthiness; +static inline JitOptRef +PyJitRef_WrapInvalid(void *ptr) +{ + return (JitOptRef){.bits = REF_CLEAR_TAG((uintptr_t)ptr) | REF_IS_INVALID}; +} -typedef union _jit_opt_symbol { - uint8_t tag; - JitOptKnownClass cls; - JitOptKnownValue value; - JitOptKnownVersion version; - JitOptTuple tuple; - JitOptTruthiness truthiness; -} JitOptSymbol; +static inline bool +PyJitRef_IsInvalid(JitOptRef ref) +{ + return REF_GET_TAG(ref.bits) == REF_IS_INVALID; +} +static inline JitOptRef +PyJitRef_MakeUnique(JitOptRef ref) +{ + return (JitOptRef){ REF_CLEAR_TAG(ref.bits) | REF_IS_UNIQUE }; +} +static inline bool +PyJitRef_IsUnique(JitOptRef ref) +{ + return REF_GET_TAG(ref.bits) == REF_IS_UNIQUE; +} -struct _Py_UOpsAbstractFrame { - // Max stacklen - int stack_len; - int locals_len; +static inline JitOptRef +PyJitRef_StripBorrowInfo(JitOptRef ref) +{ + if (PyJitRef_IsUnique(ref)) { + return ref; + } + return (JitOptRef){ .bits = REF_CLEAR_TAG(ref.bits) }; +} - JitOptSymbol **stack_pointer; - JitOptSymbol **stack; - JitOptSymbol **locals; -}; +static inline JitOptRef +PyJitRef_StripReferenceInfo(JitOptRef ref) +{ + return PyJitRef_Wrap(PyJitRef_Unwrap(ref)); +} -typedef struct _Py_UOpsAbstractFrame _Py_UOpsAbstractFrame; +static inline JitOptRef +PyJitRef_RemoveUnique(JitOptRef ref) +{ + if (PyJitRef_IsUnique(ref)) { + ref = PyJitRef_StripReferenceInfo(ref); + } + return ref; +} -typedef struct ty_arena { - int ty_curr_number; - int ty_max_number; - JitOptSymbol arena[TY_ARENA_SIZE]; -} ty_arena; +static inline JitOptRef +PyJitRef_Borrow(JitOptRef ref) +{ + return (JitOptRef){ .bits = REF_CLEAR_TAG(ref.bits) | REF_IS_BORROWED }; +} -typedef struct _JitOptContext { - char done; - char out_of_space; - bool contradiction; - // The current "executing" frame. - _Py_UOpsAbstractFrame *frame; - _Py_UOpsAbstractFrame frames[MAX_ABSTRACT_FRAME_DEPTH]; - int curr_frame_depth; +static const JitOptRef PyJitRef_NULL = {.bits = REF_IS_BORROWED}; - // Arena for the symbolic types. - ty_arena t_arena; +static inline bool +PyJitRef_IsNull(JitOptRef ref) +{ + return ref.bits == PyJitRef_NULL.bits; +} - JitOptSymbol **n_consumed; - JitOptSymbol **limit; - JitOptSymbol *locals_and_stack[MAX_ABSTRACT_INTERP_SIZE]; -} JitOptContext; +static inline int +PyJitRef_IsBorrowed(JitOptRef ref) +{ + return REF_GET_TAG(ref.bits) == REF_IS_BORROWED; +} -extern bool _Py_uop_sym_is_null(JitOptSymbol *sym); -extern bool _Py_uop_sym_is_not_null(JitOptSymbol *sym); -extern bool _Py_uop_sym_is_const(JitOptContext *ctx, JitOptSymbol *sym); -extern PyObject *_Py_uop_sym_get_const(JitOptContext *ctx, JitOptSymbol *sym); -extern JitOptSymbol *_Py_uop_sym_new_unknown(JitOptContext *ctx); -extern JitOptSymbol *_Py_uop_sym_new_not_null(JitOptContext *ctx); -extern JitOptSymbol *_Py_uop_sym_new_type( +extern bool _Py_uop_sym_is_null(JitOptRef sym); +extern bool _Py_uop_sym_is_not_null(JitOptRef sym); +extern bool _Py_uop_sym_is_const(JitOptContext *ctx, JitOptRef sym); +extern PyObject *_Py_uop_sym_get_const(JitOptContext *ctx, JitOptRef sym); +extern JitOptRef _Py_uop_sym_new_unknown(JitOptContext *ctx); +extern JitOptRef _Py_uop_sym_new_not_null(JitOptContext *ctx); +extern JitOptRef _Py_uop_sym_new_type( JitOptContext *ctx, PyTypeObject *typ); -extern JitOptSymbol *_Py_uop_sym_new_const(JitOptContext *ctx, PyObject *const_val); -extern JitOptSymbol *_Py_uop_sym_new_null(JitOptContext *ctx); -extern bool _Py_uop_sym_has_type(JitOptSymbol *sym); -extern bool _Py_uop_sym_matches_type(JitOptSymbol *sym, PyTypeObject *typ); -extern bool _Py_uop_sym_matches_type_version(JitOptSymbol *sym, unsigned int version); -extern void _Py_uop_sym_set_null(JitOptContext *ctx, JitOptSymbol *sym); -extern void _Py_uop_sym_set_non_null(JitOptContext *ctx, JitOptSymbol *sym); -extern void _Py_uop_sym_set_type(JitOptContext *ctx, JitOptSymbol *sym, PyTypeObject *typ); -extern bool _Py_uop_sym_set_type_version(JitOptContext *ctx, JitOptSymbol *sym, unsigned int version); -extern void _Py_uop_sym_set_const(JitOptContext *ctx, JitOptSymbol *sym, PyObject *const_val); -extern bool _Py_uop_sym_is_bottom(JitOptSymbol *sym); -extern int _Py_uop_sym_truthiness(JitOptContext *ctx, JitOptSymbol *sym); -extern PyTypeObject *_Py_uop_sym_get_type(JitOptSymbol *sym); -extern bool _Py_uop_sym_is_immortal(JitOptSymbol *sym); -extern JitOptSymbol *_Py_uop_sym_new_tuple(JitOptContext *ctx, int size, JitOptSymbol **args); -extern JitOptSymbol *_Py_uop_sym_tuple_getitem(JitOptContext *ctx, JitOptSymbol *sym, int item); -extern int _Py_uop_sym_tuple_length(JitOptSymbol *sym); -extern JitOptSymbol *_Py_uop_sym_new_truthiness(JitOptContext *ctx, JitOptSymbol *value, bool truthy); - -extern void _Py_uop_abstractcontext_init(JitOptContext *ctx); + +extern JitOptRef _Py_uop_sym_new_const(JitOptContext *ctx, PyObject *const_val); +extern JitOptRef _Py_uop_sym_new_const_steal(JitOptContext *ctx, PyObject *const_val); +bool _Py_uop_sym_is_safe_const(JitOptContext *ctx, JitOptRef sym); +bool _Py_uop_sym_is_not_container(JitOptRef sym); +_PyStackRef _Py_uop_sym_get_const_as_stackref(JitOptContext *ctx, JitOptRef sym); +extern JitOptRef _Py_uop_sym_new_null(JitOptContext *ctx); +extern bool _Py_uop_sym_has_type(JitOptRef sym); +extern bool _Py_uop_sym_matches_type(JitOptRef sym, PyTypeObject *typ); +extern bool _Py_uop_sym_matches_type_version(JitOptRef sym, unsigned int version); +extern unsigned int _Py_uop_sym_get_type_version(JitOptRef sym); +extern void _Py_uop_sym_set_null(JitOptContext *ctx, JitOptRef sym); +extern void _Py_uop_sym_set_non_null(JitOptContext *ctx, JitOptRef sym); +extern void _Py_uop_sym_set_type(JitOptContext *ctx, JitOptRef sym, PyTypeObject *typ); +extern bool _Py_uop_sym_set_type_version(JitOptContext *ctx, JitOptRef sym, unsigned int version); +extern void _Py_uop_sym_set_const(JitOptContext *ctx, JitOptRef sym, PyObject *const_val); +extern bool _Py_uop_sym_is_bottom(JitOptRef sym); +extern int _Py_uop_sym_truthiness(JitOptContext *ctx, JitOptRef sym); +extern PyTypeObject *_Py_uop_sym_get_type(JitOptRef sym); +extern JitOptRef _Py_uop_sym_new_tuple(JitOptContext *ctx, int size, JitOptRef *args); +extern JitOptRef _Py_uop_sym_tuple_getitem(JitOptContext *ctx, JitOptRef sym, Py_ssize_t item); +extern Py_ssize_t _Py_uop_sym_tuple_length(JitOptRef sym); +extern JitOptRef _Py_uop_sym_new_truthiness(JitOptContext *ctx, JitOptRef value, bool truthy); +extern bool _Py_uop_sym_is_compact_int(JitOptRef sym); +extern JitOptRef _Py_uop_sym_new_compact_int(JitOptContext *ctx); +extern void _Py_uop_sym_set_compact_int(JitOptContext *ctx, JitOptRef sym); +extern JitOptRef _Py_uop_sym_new_predicate(JitOptContext *ctx, JitOptRef lhs_ref, JitOptRef rhs_ref, JitOptPredicateKind kind); +extern void _Py_uop_sym_apply_predicate_narrowing(JitOptContext *ctx, JitOptRef sym, bool branch_is_true); +extern void _Py_uop_sym_set_recorded_value(JitOptContext *ctx, JitOptRef sym, PyObject *value); +extern void _Py_uop_sym_set_recorded_type(JitOptContext *ctx, JitOptRef sym, PyTypeObject *type); +extern void _Py_uop_sym_set_recorded_gen_func(JitOptContext *ctx, JitOptRef ref, PyFunctionObject *value); +extern PyCodeObject *_Py_uop_sym_get_probable_func_code(JitOptRef sym); +extern PyObject *_Py_uop_sym_get_probable_value(JitOptRef sym); +extern PyTypeObject *_Py_uop_sym_get_probable_type(JitOptRef sym); +extern JitOptRef *_Py_uop_sym_set_stack_depth(JitOptContext *ctx, int stack_depth, JitOptRef *current_sp); + +extern void _Py_uop_abstractcontext_init(JitOptContext *ctx, _PyBloomFilter *dependencies); extern void _Py_uop_abstractcontext_fini(JitOptContext *ctx); extern _Py_UOpsAbstractFrame *_Py_uop_frame_new( JitOptContext *ctx, PyCodeObject *co, - int curr_stackentries, - JitOptSymbol **args, + JitOptRef *args, + int arg_len); + +extern _Py_UOpsAbstractFrame *_Py_uop_frame_new_from_symbol( + JitOptContext *ctx, + JitOptRef callable, + JitOptRef *args, int arg_len); -extern int _Py_uop_frame_pop(JitOptContext *ctx); + +extern int _Py_uop_frame_pop(JitOptContext *ctx, PyCodeObject *co); PyAPI_FUNC(PyObject *) _Py_uop_symbols_test(PyObject *self, PyObject *ignored); -PyAPI_FUNC(int) _PyOptimizer_Optimize(_PyInterpreterFrame *frame, _Py_CODEUNIT *start, _PyExecutorObject **exec_ptr, int chain_depth); +PyAPI_FUNC(int) _PyOptimizer_Optimize(_PyInterpreterFrame *frame, PyThreadState *tstate); -static inline int is_terminator(const _PyUOpInstruction *uop) +static inline _PyExecutorObject *_PyExecutor_FromExit(_PyExitData *exit) { - int opcode = uop->opcode; - return ( - opcode == _EXIT_TRACE || - opcode == _JUMP_TO_TOP - ); + _PyExitData *exit0 = exit - exit->index; + return (_PyExecutorObject *)(((char *)exit0) - offsetof(_PyExecutorObject, exits)); } +extern _PyExecutorObject *_PyExecutor_GetColdExecutor(void); +extern _PyExecutorObject *_PyExecutor_GetColdDynamicExecutor(void); + +PyAPI_FUNC(void) _PyExecutor_ClearExit(_PyExitData *exit); + +extern void _PyExecutor_Free(_PyExecutorObject *self); + PyAPI_FUNC(int) _PyDumpExecutors(FILE *out); #ifdef _Py_TIER2 -extern void _Py_ClearExecutorDeletionList(PyInterpreterState *interp); +PyAPI_FUNC(void) _Py_ClearExecutorDeletionList(PyInterpreterState *interp); +#endif + +PyAPI_FUNC(int) _PyJit_translate_single_bytecode_to_trace(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *next_instr, int stop_tracing_opcode); + +PyAPI_FUNC(int) +_PyJit_TryInitializeTracing(PyThreadState *tstate, _PyInterpreterFrame *frame, + _Py_CODEUNIT *curr_instr, _Py_CODEUNIT *start_instr, + _Py_CODEUNIT *close_loop_instr, _PyStackRef *stack_pointer, int chain_depth, _PyExitData *exit, + int oparg, _PyExecutorObject *current_executor); + +PyAPI_FUNC(void) _PyJit_FinalizeTracing(PyThreadState *tstate, int err); +PyAPI_FUNC(bool) _PyJit_EnterExecutorShouldStopTracing(int og_opcode); + +void _PyPrintExecutor(_PyExecutorObject *executor, const _PyUOpInstruction *marker); +void _PyJit_TracerFree(_PyThreadStateImpl *_tstate); + +#ifdef _Py_TIER2 +typedef void (*_Py_RecordFuncPtr)(_PyInterpreterFrame *frame, _PyStackRef *stackpointer, int oparg, PyObject **recorded_value); +PyAPI_DATA(const _Py_RecordFuncPtr) _PyOpcode_RecordFunctions[]; + +typedef struct { + uint8_t count; + uint8_t indices[MAX_RECORDED_VALUES]; +} _PyOpcodeRecordEntry; + +typedef struct { + uint8_t count; + uint8_t transform_mask; + uint8_t slots[MAX_RECORDED_VALUES]; +} _PyOpcodeRecordSlotMap; + +PyAPI_DATA(const _PyOpcodeRecordEntry) _PyOpcode_RecordEntries[256]; +PyAPI_DATA(const _PyOpcodeRecordSlotMap) _PyOpcode_RecordSlotMaps[256]; + +/* Convert a family-recorded value to the form a recorder uop expects. + * If no transform is needed, return the input value unchanged. + * Takes ownership of `value` and returns a new strong reference or NULL. + */ +PyAPI_FUNC(PyObject *) _PyOpcode_RecordTransformValue(int uop, PyObject *value); #endif #ifdef __cplusplus diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_optimizer_types.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_optimizer_types.h new file mode 100644 index 000000000..a722652cc --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_optimizer_types.h @@ -0,0 +1,169 @@ +#ifndef Py_INTERNAL_OPTIMIZER_TYPES_H +#define Py_INTERNAL_OPTIMIZER_TYPES_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include +#include "pycore_uop.h" // UOP_MAX_TRACE_LENGTH + +#define ABSTRACT_INTERP_STACK_SIZE 256 +#define ABSTRACT_INTERP_LOCALS_SIZE 512 + + +#define TY_ARENA_SIZE (UOP_MAX_TRACE_LENGTH * 5) + +// Need extras for root frame and for overflow frame (see TRACE_STACK_PUSH()) +#define MAX_ABSTRACT_FRAME_DEPTH (16) + +// The maximum number of side exits that we can take before requiring forward +// progress (and inserting a new ENTER_EXECUTOR instruction). In practice, this +// is the "maximum amount of polymorphism" that an isolated trace tree can +// handle before rejoining the rest of the program. +#define MAX_CHAIN_DEPTH 4 + +/* Symbols */ +/* See explanation in optimizer_symbols.c */ + + +typedef enum _JitSymType { + JIT_SYM_UNKNOWN_TAG = 1, + JIT_SYM_NULL_TAG = 2, + JIT_SYM_NON_NULL_TAG = 3, + JIT_SYM_BOTTOM_TAG = 4, + JIT_SYM_TYPE_VERSION_TAG = 5, + JIT_SYM_KNOWN_CLASS_TAG = 7, + JIT_SYM_KNOWN_VALUE_TAG = 8, + JIT_SYM_TUPLE_TAG = 9, + JIT_SYM_TRUTHINESS_TAG = 10, + JIT_SYM_COMPACT_INT = 11, + JIT_SYM_PREDICATE_TAG = 12, + JIT_SYM_RECORDED_VALUE_TAG = 13, + JIT_SYM_RECORDED_TYPE_TAG = 14, + JIT_SYM_RECORDED_GEN_FUNC_TAG = 15, +} JitSymType; + +typedef struct _jit_opt_known_class { + uint8_t tag; + uint32_t version; + PyTypeObject *type; +} JitOptKnownClass; + +typedef struct _jit_opt_known_version { + uint8_t tag; + uint32_t version; +} JitOptKnownVersion; + +typedef struct _jit_opt_known_func_version { + uint8_t tag; + uint32_t func_version; +} JitOptKnownFuncVersion; + +typedef struct _jit_opt_known_value { + uint8_t tag; + PyObject *value; +} JitOptKnownValue; + +#define MAX_SYMBOLIC_TUPLE_SIZE 7 + +typedef struct _jit_opt_tuple { + uint8_t tag; + uint8_t length; + uint16_t items[MAX_SYMBOLIC_TUPLE_SIZE]; +} JitOptTuple; + +typedef struct { + uint8_t tag; + bool invert; + uint16_t value; +} JitOptTruthiness; + +typedef enum { + JIT_PRED_IS, + JIT_PRED_IS_NOT, + JIT_PRED_EQ, + JIT_PRED_NE, +} JitOptPredicateKind; + +typedef struct { + uint8_t tag; + uint8_t kind; + uint16_t lhs; + uint16_t rhs; +} JitOptPredicate; + +typedef struct _jit_opt_recorded_value { + uint8_t tag; + bool known_type; + PyObject *value; +} JitOptRecordedValue; + +typedef struct _jit_opt_recorded_type { + uint8_t tag; + PyTypeObject *type; +} JitOptRecordedType; + +/* Represents a generator, but we record the + * function as the generator is emphemeral */ +typedef struct _jit_opt_recorded_gen_func { + uint8_t tag; + PyFunctionObject *func; +} JitOptRecordedGenFunc; + +typedef struct { + uint8_t tag; +} JitOptCompactInt; + +typedef union _jit_opt_symbol { + uint8_t tag; + JitOptKnownClass cls; + JitOptKnownValue value; + JitOptKnownVersion version; + JitOptKnownFuncVersion func_version; + JitOptTuple tuple; + JitOptTruthiness truthiness; + JitOptCompactInt compact; + JitOptPredicate predicate; + JitOptRecordedValue recorded_value; + JitOptRecordedType recorded_type; + JitOptRecordedGenFunc recorded_gen_func; +} JitOptSymbol; + +// This mimics the _PyStackRef API +typedef union { + uintptr_t bits; +} JitOptRef; + +typedef struct _Py_UOpsAbstractFrame { + bool globals_watched; + // The version number of the globals dicts, once checked. 0 if unchecked. + uint32_t globals_checked_version; + // Max stacklen + int stack_len; + int locals_len; + bool caller; // We have made a call from this frame during the trace + bool is_c_recursion_checked; + JitOptRef callable; + PyFunctionObject *func; + PyCodeObject *code; + + JitOptRef *stack_pointer; + JitOptRef *stack; + JitOptRef *locals; +} _Py_UOpsAbstractFrame; + +typedef struct ty_arena { + int ty_curr_number; + int ty_max_number; + JitOptSymbol arena[TY_ARENA_SIZE]; +} ty_arena; + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_OPTIMIZER_TYPES_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_parser.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_parser.h index 2885dee63..b89d02035 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_parser.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_parser.h @@ -14,10 +14,8 @@ extern "C" { #include "pycore_pyarena.h" // PyArena _Py_DECLARE_STR(empty, "") -#if defined(Py_DEBUG) && defined(Py_GIL_DISABLED) #define _parser_runtime_state_INIT \ { \ - .mutex = {0}, \ .dummy_name = { \ .kind = Name_kind, \ .v.Name.id = &_Py_STR(empty), \ @@ -28,27 +26,14 @@ _Py_DECLARE_STR(empty, "") .end_col_offset = 0, \ }, \ } -#else -#define _parser_runtime_state_INIT \ - { \ - .dummy_name = { \ - .kind = Name_kind, \ - .v.Name.id = &_Py_STR(empty), \ - .v.Name.ctx = Load, \ - .lineno = 1, \ - .col_offset = 0, \ - .end_lineno = 1, \ - .end_col_offset = 0, \ - }, \ - } -#endif extern struct _mod* _PyParser_ASTFromString( const char *str, PyObject* filename, int mode, PyCompilerFlags *flags, - PyArena *arena); + PyArena *arena, + PyObject *module); extern struct _mod* _PyParser_ASTFromFile( FILE *fp, diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyatomic_ft_wrappers.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyatomic_ft_wrappers.h index bcb9e01a5..d8ec306a0 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyatomic_ft_wrappers.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyatomic_ft_wrappers.h @@ -41,6 +41,8 @@ extern "C" { _Py_atomic_load_uint8(&value) #define FT_ATOMIC_STORE_UINT8(value, new_value) \ _Py_atomic_store_uint8(&value, new_value) +#define FT_ATOMIC_LOAD_INT8_RELAXED(value) \ + _Py_atomic_load_int8_relaxed(&value) #define FT_ATOMIC_LOAD_UINT8_RELAXED(value) \ _Py_atomic_load_uint8_relaxed(&value) #define FT_ATOMIC_LOAD_UINT16_RELAXED(value) \ @@ -59,8 +61,14 @@ extern "C" { _Py_atomic_store_ptr_release(&value, new_value) #define FT_ATOMIC_STORE_UINTPTR_RELEASE(value, new_value) \ _Py_atomic_store_uintptr_release(&value, new_value) +#define FT_ATOMIC_STORE_INT8_RELAXED(value, new_value) \ + _Py_atomic_store_int8_relaxed(&value, new_value) +#define FT_ATOMIC_STORE_INT8_RELEASE(value, new_value) \ + _Py_atomic_store_int8_release(&value, new_value) #define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) \ _Py_atomic_store_ssize_relaxed(&value, new_value) +#define FT_ATOMIC_STORE_SSIZE_RELEASE(value, new_value) \ + _Py_atomic_store_ssize_release(&value, new_value) #define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) \ _Py_atomic_store_uint8_relaxed(&value, new_value) #define FT_ATOMIC_STORE_UINT16_RELAXED(value, new_value) \ @@ -111,8 +119,6 @@ extern "C" { _Py_atomic_store_ulong_relaxed(&value, new_value) #define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) \ _Py_atomic_store_ssize_relaxed(&value, new_value) -#define FT_ATOMIC_STORE_SSIZE_RELEASE(value, new_value) \ - _Py_atomic_store_ssize_release(&value, new_value) #define FT_ATOMIC_STORE_FLOAT_RELAXED(value, new_value) \ _Py_atomic_store_float_relaxed(&value, new_value) #define FT_ATOMIC_LOAD_FLOAT_RELAXED(value) \ @@ -147,6 +153,7 @@ extern "C" { #define FT_ATOMIC_LOAD_PTR_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT8(value) value #define FT_ATOMIC_STORE_UINT8(value, new_value) value = new_value +#define FT_ATOMIC_LOAD_INT8_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT8_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT16_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value @@ -156,6 +163,8 @@ extern "C" { #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value #define FT_ATOMIC_STORE_UINTPTR_RELEASE(value, new_value) value = new_value +#define FT_ATOMIC_STORE_INT8_RELAXED(value, new_value) value = new_value +#define FT_ATOMIC_STORE_INT8_RELEASE(value, new_value) value = new_value #define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_SSIZE_RELEASE(value, new_value) value = new_value #define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) value = new_value diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyerrors.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyerrors.h index 2c2048f7e..c1f9d71e4 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyerrors.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pyerrors.h @@ -11,8 +11,8 @@ extern "C" { /* Error handling definitions */ -extern _PyErr_StackItem* _PyErr_GetTopmostException(PyThreadState *tstate); -extern PyObject* _PyErr_GetHandledException(PyThreadState *); +PyAPI_FUNC(_PyErr_StackItem*) _PyErr_GetTopmostException(PyThreadState *tstate); +PyAPI_FUNC(PyObject*) _PyErr_GetHandledException(PyThreadState *); extern void _PyErr_SetHandledException(PyThreadState *, PyObject *); extern void _PyErr_GetExcInfo(PyThreadState *, PyObject **, PyObject **, PyObject **); @@ -29,7 +29,8 @@ PyAPI_FUNC(PyObject*) _PyErr_FormatFromCause( ... ); -extern int _PyException_AddNote( +// Export for 'pyexpat' shared extension. +PyAPI_FUNC(int) _PyException_AddNote( PyObject *exc, PyObject *note); @@ -108,7 +109,7 @@ extern void _PyErr_Restore( PyObject *value, PyObject *traceback); -extern void _PyErr_SetObject( +PyAPI_FUNC(void) _PyErr_SetObject( PyThreadState *tstate, PyObject *type, PyObject *value); @@ -122,8 +123,11 @@ extern void _PyErr_SetNone(PyThreadState *tstate, PyObject *exception); extern PyObject* _PyErr_NoMemory(PyThreadState *tstate); -extern int _PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename, int lineno, int col_offset, - int end_lineno, int end_col_offset); +// Export for test_peg_generator +PyAPI_FUNC(int) _PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename, + int lineno, int col_offset, + int end_lineno, int end_col_offset, + PyObject *module); extern void _PyErr_RaiseSyntaxError(PyObject *msg, PyObject *filename, int lineno, int col_offset, int end_lineno, int end_col_offset); @@ -168,7 +172,8 @@ extern PyObject* _PyErr_FormatFromCauseTstate( const char *format, ...); -extern PyObject* _PyExc_CreateExceptionGroup( +// Exported for external JIT support +PyAPI_FUNC(PyObject *) _PyExc_CreateExceptionGroup( const char *msg, PyObject *excs); @@ -179,7 +184,8 @@ extern PyObject* _PyExc_PrepReraiseStar( extern int _PyErr_CheckSignalsTstate(PyThreadState *tstate); extern void _Py_DumpExtensionModules(int fd, PyInterpreterState *interp); -extern PyObject* _Py_CalculateSuggestions(PyObject *dir, PyObject *name); +// Exported for external JIT support +PyAPI_FUNC(PyObject *) _Py_CalculateSuggestions(PyObject *dir, PyObject *name); extern PyObject* _Py_Offer_Suggestions(PyObject* exception); // Export for '_testinternalcapi' shared extension diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymath.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymath.h index 5bbbdaab8..532c5ceaf 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymath.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymath.h @@ -33,7 +33,7 @@ extern "C" { static inline void _Py_ADJUST_ERANGE1(double x) { if (errno == 0) { - if (x == Py_INFINITY || x == -Py_INFINITY) { + if (x == INFINITY || x == -INFINITY) { errno = ERANGE; } } @@ -44,8 +44,8 @@ static inline void _Py_ADJUST_ERANGE1(double x) static inline void _Py_ADJUST_ERANGE2(double x, double y) { - if (x == Py_INFINITY || x == -Py_INFINITY || - y == Py_INFINITY || y == -Py_INFINITY) + if (x == INFINITY || x == -INFINITY || + y == INFINITY || y == -INFINITY) { if (errno == 0) { errno = ERANGE; @@ -182,8 +182,7 @@ extern void _Py_set_387controlword(unsigned short); // (extended precision), and we don't know how to change // the rounding precision. #if !defined(DOUBLE_IS_LITTLE_ENDIAN_IEEE754) && \ - !defined(DOUBLE_IS_BIG_ENDIAN_IEEE754) && \ - !defined(DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754) + !defined(DOUBLE_IS_BIG_ENDIAN_IEEE754) # define _PY_SHORT_FLOAT_REPR 0 #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem.h index f0b90a255..05484e847 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem.h @@ -113,16 +113,6 @@ extern int _PyMem_DebugEnabled(void); // Enqueue a pointer to be freed possibly after some delay. extern void _PyMem_FreeDelayed(void *ptr, size_t size); -// Enqueue an object to be freed possibly after some delay -#ifdef Py_GIL_DISABLED -PyAPI_FUNC(void) _PyObject_XDecRefDelayed(PyObject *obj); -#else -static inline void _PyObject_XDecRefDelayed(PyObject *obj) -{ - Py_XDECREF(obj); -} -#endif - // Periodically process delayed free requests. extern void _PyMem_ProcessDelayed(PyThreadState *tstate); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem_init.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem_init.h index c593edc86..2a0e0817d 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem_init.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pymem_init.h @@ -30,6 +30,12 @@ extern void* _PyMem_MiCalloc(void *, size_t, size_t); extern void _PyMem_MiFree(void *, void *); extern void* _PyMem_MiRealloc(void *, void *, size_t); # define PYMEM_ALLOC {NULL, _PyMem_MiMalloc, _PyMem_MiCalloc, _PyMem_MiRealloc, _PyMem_MiFree} +extern void* _PyMem_MiRawMalloc(void *, size_t); +extern void* _PyMem_MiRawCalloc(void *, size_t, size_t); +extern void _PyMem_MiRawFree(void *, void *); +extern void* _PyMem_MiRawRealloc(void *, void *, size_t); +# undef PYRAW_ALLOC +# define PYRAW_ALLOC {NULL, _PyMem_MiRawMalloc, _PyMem_MiRawCalloc, _PyMem_MiRawRealloc, _PyMem_MiRawFree} #elif defined(WITH_PYMALLOC) extern void* _PyObject_Malloc(void *, size_t); extern void* _PyObject_Calloc(void *, size_t, size_t); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystate.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystate.h index 7f1b0b752..189bf7d71 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystate.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystate.h @@ -89,8 +89,9 @@ _Py_ThreadCanHandleSignals(PyInterpreterState *interp) /* Variable and static inline functions for in-line access to current thread and interpreter state */ -#if defined(HAVE_THREAD_LOCAL) && !defined(Py_BUILD_CORE_MODULE) +#if !defined(Py_BUILD_CORE_MODULE) extern _Py_thread_local PyThreadState *_Py_tss_tstate; +extern _Py_thread_local PyInterpreterState *_Py_tss_interp; #endif #ifndef NDEBUG @@ -114,7 +115,7 @@ PyAPI_FUNC(PyThreadState *) _PyThreadState_GetCurrent(void); static inline PyThreadState* _PyThreadState_GET(void) { -#if defined(HAVE_THREAD_LOCAL) && !defined(Py_BUILD_CORE_MODULE) +#if !defined(Py_BUILD_CORE_MODULE) return _Py_tss_tstate; #else return _PyThreadState_GetCurrent(); @@ -149,6 +150,23 @@ extern void _PyThreadState_Detach(PyThreadState *tstate); // to the "detached" state. extern void _PyThreadState_Suspend(PyThreadState *tstate); +#ifdef Py_GIL_DISABLED +// Try to atomically transition a *different* thread's state from "detached" +// to "suspended". On success, the target thread cannot attach until +// _PyThreadState_ResumeDetached() is called, and the caller may safely +// perform operations that are normally only permitted for the owning thread +// (such as merging the biased reference counts of objects it owns). +// +// The caller must not run arbitrary Python code, allocate GC objects, or +// stop the world while holding the thread in the suspended state. +// Returns 1 on success, 0 if the thread was not in the "detached" state. +extern int _PyThreadState_TrySuspendDetached(PyThreadState *tstate); + +// Undo a successful _PyThreadState_TrySuspendDetached(): switch the thread +// back to "detached" and wake it if it is waiting to attach. +extern void _PyThreadState_ResumeDetached(PyThreadState *tstate); +#endif + // Mark the thread state as "shutting down". This is used during interpreter // and runtime finalization. The thread may no longer attach to the // interpreter and will instead block via _PyThreadState_HangThread(). @@ -204,11 +222,15 @@ _Py_EnsureFuncTstateNotNULL(const char *func, PyThreadState *tstate) See also PyInterpreterState_Get() and _PyGILState_GetInterpreterStateUnsafe(). */ static inline PyInterpreterState* _PyInterpreterState_GET(void) { - PyThreadState *tstate = _PyThreadState_GET(); #ifdef Py_DEBUG + PyThreadState *tstate = _PyThreadState_GET(); _Py_EnsureTstateNotNULL(tstate); #endif - return tstate->interp; +#if !defined(Py_BUILD_CORE_MODULE) + return _Py_tss_interp; +#else + return _PyThreadState_GET()->interp; +#endif } @@ -261,6 +283,8 @@ extern int _PyOS_InterruptOccurred(PyThreadState *tstate); PyMutex_LockFlags(&(runtime)->interpreters.mutex, _Py_LOCK_DONT_DETACH) #define HEAD_UNLOCK(runtime) \ PyMutex_Unlock(&(runtime)->interpreters.mutex) +#define ASSERT_HEAD_IS_LOCKED(runtime) \ + assert(PyMutex_IsLocked(&(runtime)->interpreters.mutex)) #define _Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) \ for (PyThreadState *t = interp->threads.head; t; t = t->next) @@ -333,6 +357,24 @@ _Py_RecursionLimit_GetMargin(PyThreadState *tstate) #endif } +/* PEP 788 structures. */ + +struct PyInterpreterGuard { + PyInterpreterState *interp; +}; + +struct PyInterpreterView { + int64_t id; +}; + +// Exports for '_testinternalcapi' shared extension +PyAPI_FUNC(Py_ssize_t) _PyInterpreterState_GuardCountdown(PyInterpreterState *interp); +PyAPI_FUNC(PyInterpreterState *) _PyInterpreterGuard_GetInterpreter(PyInterpreterGuard *guard); + +extern int _PyInterpreterGuard_TryAcquire(PyInterpreterState *interp, + PyInterpreterGuard *guard); +extern void _PyInterpreterGuard_Release(PyInterpreterGuard *guard); + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystats.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystats.h index f8af398a5..50ab21aa0 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystats.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pystats.h @@ -9,7 +9,7 @@ extern "C" { #endif #ifdef Py_STATS -extern void _Py_StatsOn(void); +extern int _Py_StatsOn(void); extern void _Py_StatsOff(void); extern void _Py_StatsClear(void); extern int _Py_PrintSpecializationStats(int to_file); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pythonrun.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pythonrun.h index 5dd93bd61..c58ae9bb7 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pythonrun.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_pythonrun.h @@ -28,6 +28,12 @@ extern const char* _Py_SourceAsString( PyCompilerFlags *cf, PyObject **cmd_copy); +extern PyObject * _Py_CompileString( + const char *str, + PyObject *filename, int start, + PyCompilerFlags *flags, int optimize, + PyObject *module); + // Export for special main.c string compiling with source tracebacks extern PyObject* _PyRun_SimpleString( const char *command, diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime.h index 7fc7f343f..fcd2ae9b1 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime.h @@ -56,6 +56,29 @@ _PyRuntimeState_SetFinalizing(_PyRuntimeState *runtime, PyThreadState *tstate) { } } +// Atomic so a thread that reads initialized=1 observes all writes +// from the initialization sequence (gh-146302). + +static inline int +_PyRuntimeState_GetCoreInitialized(_PyRuntimeState *runtime) { + return _Py_atomic_load_int(&runtime->core_initialized); +} + +static inline void +_PyRuntimeState_SetCoreInitialized(_PyRuntimeState *runtime, int initialized) { + _Py_atomic_store_int(&runtime->core_initialized, initialized); +} + +static inline int +_PyRuntimeState_GetInitialized(_PyRuntimeState *runtime) { + return _Py_atomic_load_int(&runtime->initialized); +} + +static inline void +_PyRuntimeState_SetInitialized(_PyRuntimeState *runtime, int initialized) { + _Py_atomic_store_int(&runtime->initialized, initialized); +} + #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init.h index e250d84d2..6c48ac0dc 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init.h @@ -13,7 +13,7 @@ extern "C" { #include "pycore_debug_offsets.h" // _Py_DebugOffsets_INIT() #include "pycore_dtoa.h" // _dtoa_state_INIT() #include "pycore_faulthandler.h" // _faulthandler_runtime_state_INIT -#include "pycore_floatobject.h" // _py_float_format_unknown +#include "pycore_floatobject.h" // _py_float_format_* #include "pycore_function.h" #include "pycore_hamt.h" // _PyHamt_BitmapNode_Type #include "pycore_import.h" // IMPORTS_INIT @@ -61,9 +61,6 @@ extern PyTypeObject _PyExc_MemoryError; }, \ }, \ }, \ - /* A TSS key must be initialized with Py_tss_NEEDS_INIT \ - in accordance with the specification. */ \ - .autoTSSkey = Py_tss_NEEDS_INIT, \ .parser = _parser_runtime_state_INIT, \ .ceval = { \ .pending_mainthread = { \ @@ -87,10 +84,6 @@ extern PyTypeObject _PyExc_MemoryError; .stoptheworld = { \ .is_global = 1, \ }, \ - .float_state = { \ - .float_format = _py_float_format_unknown, \ - .double_format = _py_float_format_unknown, \ - }, \ .types = { \ .next_version_tag = _Py_TYPE_VERSION_NEXT, \ }, \ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init_generated.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init_generated.h index e642a42bb..5c6635e6c 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init_generated.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_init_generated.h @@ -275,6 +275,774 @@ extern "C" { _PyLong_DIGIT_INIT(254), \ _PyLong_DIGIT_INIT(255), \ _PyLong_DIGIT_INIT(256), \ + _PyLong_DIGIT_INIT(257), \ + _PyLong_DIGIT_INIT(258), \ + _PyLong_DIGIT_INIT(259), \ + _PyLong_DIGIT_INIT(260), \ + _PyLong_DIGIT_INIT(261), \ + _PyLong_DIGIT_INIT(262), \ + _PyLong_DIGIT_INIT(263), \ + _PyLong_DIGIT_INIT(264), \ + _PyLong_DIGIT_INIT(265), \ + _PyLong_DIGIT_INIT(266), \ + _PyLong_DIGIT_INIT(267), \ + _PyLong_DIGIT_INIT(268), \ + _PyLong_DIGIT_INIT(269), \ + _PyLong_DIGIT_INIT(270), \ + _PyLong_DIGIT_INIT(271), \ + _PyLong_DIGIT_INIT(272), \ + _PyLong_DIGIT_INIT(273), \ + _PyLong_DIGIT_INIT(274), \ + _PyLong_DIGIT_INIT(275), \ + _PyLong_DIGIT_INIT(276), \ + _PyLong_DIGIT_INIT(277), \ + _PyLong_DIGIT_INIT(278), \ + _PyLong_DIGIT_INIT(279), \ + _PyLong_DIGIT_INIT(280), \ + _PyLong_DIGIT_INIT(281), \ + _PyLong_DIGIT_INIT(282), \ + _PyLong_DIGIT_INIT(283), \ + _PyLong_DIGIT_INIT(284), \ + _PyLong_DIGIT_INIT(285), \ + _PyLong_DIGIT_INIT(286), \ + _PyLong_DIGIT_INIT(287), \ + _PyLong_DIGIT_INIT(288), \ + _PyLong_DIGIT_INIT(289), \ + _PyLong_DIGIT_INIT(290), \ + _PyLong_DIGIT_INIT(291), \ + _PyLong_DIGIT_INIT(292), \ + _PyLong_DIGIT_INIT(293), \ + _PyLong_DIGIT_INIT(294), \ + _PyLong_DIGIT_INIT(295), \ + _PyLong_DIGIT_INIT(296), \ + _PyLong_DIGIT_INIT(297), \ + _PyLong_DIGIT_INIT(298), \ + _PyLong_DIGIT_INIT(299), \ + _PyLong_DIGIT_INIT(300), \ + _PyLong_DIGIT_INIT(301), \ + _PyLong_DIGIT_INIT(302), \ + _PyLong_DIGIT_INIT(303), \ + _PyLong_DIGIT_INIT(304), \ + _PyLong_DIGIT_INIT(305), \ + _PyLong_DIGIT_INIT(306), \ + _PyLong_DIGIT_INIT(307), \ + _PyLong_DIGIT_INIT(308), \ + _PyLong_DIGIT_INIT(309), \ + _PyLong_DIGIT_INIT(310), \ + _PyLong_DIGIT_INIT(311), \ + _PyLong_DIGIT_INIT(312), \ + _PyLong_DIGIT_INIT(313), \ + _PyLong_DIGIT_INIT(314), \ + _PyLong_DIGIT_INIT(315), \ + _PyLong_DIGIT_INIT(316), \ + _PyLong_DIGIT_INIT(317), \ + _PyLong_DIGIT_INIT(318), \ + _PyLong_DIGIT_INIT(319), \ + _PyLong_DIGIT_INIT(320), \ + _PyLong_DIGIT_INIT(321), \ + _PyLong_DIGIT_INIT(322), \ + _PyLong_DIGIT_INIT(323), \ + _PyLong_DIGIT_INIT(324), \ + _PyLong_DIGIT_INIT(325), \ + _PyLong_DIGIT_INIT(326), \ + _PyLong_DIGIT_INIT(327), \ + _PyLong_DIGIT_INIT(328), \ + _PyLong_DIGIT_INIT(329), \ + _PyLong_DIGIT_INIT(330), \ + _PyLong_DIGIT_INIT(331), \ + _PyLong_DIGIT_INIT(332), \ + _PyLong_DIGIT_INIT(333), \ + _PyLong_DIGIT_INIT(334), \ + _PyLong_DIGIT_INIT(335), \ + _PyLong_DIGIT_INIT(336), \ + _PyLong_DIGIT_INIT(337), \ + _PyLong_DIGIT_INIT(338), \ + _PyLong_DIGIT_INIT(339), \ + _PyLong_DIGIT_INIT(340), \ + _PyLong_DIGIT_INIT(341), \ + _PyLong_DIGIT_INIT(342), \ + _PyLong_DIGIT_INIT(343), \ + _PyLong_DIGIT_INIT(344), \ + _PyLong_DIGIT_INIT(345), \ + _PyLong_DIGIT_INIT(346), \ + _PyLong_DIGIT_INIT(347), \ + _PyLong_DIGIT_INIT(348), \ + _PyLong_DIGIT_INIT(349), \ + _PyLong_DIGIT_INIT(350), \ + _PyLong_DIGIT_INIT(351), \ + _PyLong_DIGIT_INIT(352), \ + _PyLong_DIGIT_INIT(353), \ + _PyLong_DIGIT_INIT(354), \ + _PyLong_DIGIT_INIT(355), \ + _PyLong_DIGIT_INIT(356), \ + _PyLong_DIGIT_INIT(357), \ + _PyLong_DIGIT_INIT(358), \ + _PyLong_DIGIT_INIT(359), \ + _PyLong_DIGIT_INIT(360), \ + _PyLong_DIGIT_INIT(361), \ + _PyLong_DIGIT_INIT(362), \ + _PyLong_DIGIT_INIT(363), \ + _PyLong_DIGIT_INIT(364), \ + _PyLong_DIGIT_INIT(365), \ + _PyLong_DIGIT_INIT(366), \ + _PyLong_DIGIT_INIT(367), \ + _PyLong_DIGIT_INIT(368), \ + _PyLong_DIGIT_INIT(369), \ + _PyLong_DIGIT_INIT(370), \ + _PyLong_DIGIT_INIT(371), \ + _PyLong_DIGIT_INIT(372), \ + _PyLong_DIGIT_INIT(373), \ + _PyLong_DIGIT_INIT(374), \ + _PyLong_DIGIT_INIT(375), \ + _PyLong_DIGIT_INIT(376), \ + _PyLong_DIGIT_INIT(377), \ + _PyLong_DIGIT_INIT(378), \ + _PyLong_DIGIT_INIT(379), \ + _PyLong_DIGIT_INIT(380), \ + _PyLong_DIGIT_INIT(381), \ + _PyLong_DIGIT_INIT(382), \ + _PyLong_DIGIT_INIT(383), \ + _PyLong_DIGIT_INIT(384), \ + _PyLong_DIGIT_INIT(385), \ + _PyLong_DIGIT_INIT(386), \ + _PyLong_DIGIT_INIT(387), \ + _PyLong_DIGIT_INIT(388), \ + _PyLong_DIGIT_INIT(389), \ + _PyLong_DIGIT_INIT(390), \ + _PyLong_DIGIT_INIT(391), \ + _PyLong_DIGIT_INIT(392), \ + _PyLong_DIGIT_INIT(393), \ + _PyLong_DIGIT_INIT(394), \ + _PyLong_DIGIT_INIT(395), \ + _PyLong_DIGIT_INIT(396), \ + _PyLong_DIGIT_INIT(397), \ + _PyLong_DIGIT_INIT(398), \ + _PyLong_DIGIT_INIT(399), \ + _PyLong_DIGIT_INIT(400), \ + _PyLong_DIGIT_INIT(401), \ + _PyLong_DIGIT_INIT(402), \ + _PyLong_DIGIT_INIT(403), \ + _PyLong_DIGIT_INIT(404), \ + _PyLong_DIGIT_INIT(405), \ + _PyLong_DIGIT_INIT(406), \ + _PyLong_DIGIT_INIT(407), \ + _PyLong_DIGIT_INIT(408), \ + _PyLong_DIGIT_INIT(409), \ + _PyLong_DIGIT_INIT(410), \ + _PyLong_DIGIT_INIT(411), \ + _PyLong_DIGIT_INIT(412), \ + _PyLong_DIGIT_INIT(413), \ + _PyLong_DIGIT_INIT(414), \ + _PyLong_DIGIT_INIT(415), \ + _PyLong_DIGIT_INIT(416), \ + _PyLong_DIGIT_INIT(417), \ + _PyLong_DIGIT_INIT(418), \ + _PyLong_DIGIT_INIT(419), \ + _PyLong_DIGIT_INIT(420), \ + _PyLong_DIGIT_INIT(421), \ + _PyLong_DIGIT_INIT(422), \ + _PyLong_DIGIT_INIT(423), \ + _PyLong_DIGIT_INIT(424), \ + _PyLong_DIGIT_INIT(425), \ + _PyLong_DIGIT_INIT(426), \ + _PyLong_DIGIT_INIT(427), \ + _PyLong_DIGIT_INIT(428), \ + _PyLong_DIGIT_INIT(429), \ + _PyLong_DIGIT_INIT(430), \ + _PyLong_DIGIT_INIT(431), \ + _PyLong_DIGIT_INIT(432), \ + _PyLong_DIGIT_INIT(433), \ + _PyLong_DIGIT_INIT(434), \ + _PyLong_DIGIT_INIT(435), \ + _PyLong_DIGIT_INIT(436), \ + _PyLong_DIGIT_INIT(437), \ + _PyLong_DIGIT_INIT(438), \ + _PyLong_DIGIT_INIT(439), \ + _PyLong_DIGIT_INIT(440), \ + _PyLong_DIGIT_INIT(441), \ + _PyLong_DIGIT_INIT(442), \ + _PyLong_DIGIT_INIT(443), \ + _PyLong_DIGIT_INIT(444), \ + _PyLong_DIGIT_INIT(445), \ + _PyLong_DIGIT_INIT(446), \ + _PyLong_DIGIT_INIT(447), \ + _PyLong_DIGIT_INIT(448), \ + _PyLong_DIGIT_INIT(449), \ + _PyLong_DIGIT_INIT(450), \ + _PyLong_DIGIT_INIT(451), \ + _PyLong_DIGIT_INIT(452), \ + _PyLong_DIGIT_INIT(453), \ + _PyLong_DIGIT_INIT(454), \ + _PyLong_DIGIT_INIT(455), \ + _PyLong_DIGIT_INIT(456), \ + _PyLong_DIGIT_INIT(457), \ + _PyLong_DIGIT_INIT(458), \ + _PyLong_DIGIT_INIT(459), \ + _PyLong_DIGIT_INIT(460), \ + _PyLong_DIGIT_INIT(461), \ + _PyLong_DIGIT_INIT(462), \ + _PyLong_DIGIT_INIT(463), \ + _PyLong_DIGIT_INIT(464), \ + _PyLong_DIGIT_INIT(465), \ + _PyLong_DIGIT_INIT(466), \ + _PyLong_DIGIT_INIT(467), \ + _PyLong_DIGIT_INIT(468), \ + _PyLong_DIGIT_INIT(469), \ + _PyLong_DIGIT_INIT(470), \ + _PyLong_DIGIT_INIT(471), \ + _PyLong_DIGIT_INIT(472), \ + _PyLong_DIGIT_INIT(473), \ + _PyLong_DIGIT_INIT(474), \ + _PyLong_DIGIT_INIT(475), \ + _PyLong_DIGIT_INIT(476), \ + _PyLong_DIGIT_INIT(477), \ + _PyLong_DIGIT_INIT(478), \ + _PyLong_DIGIT_INIT(479), \ + _PyLong_DIGIT_INIT(480), \ + _PyLong_DIGIT_INIT(481), \ + _PyLong_DIGIT_INIT(482), \ + _PyLong_DIGIT_INIT(483), \ + _PyLong_DIGIT_INIT(484), \ + _PyLong_DIGIT_INIT(485), \ + _PyLong_DIGIT_INIT(486), \ + _PyLong_DIGIT_INIT(487), \ + _PyLong_DIGIT_INIT(488), \ + _PyLong_DIGIT_INIT(489), \ + _PyLong_DIGIT_INIT(490), \ + _PyLong_DIGIT_INIT(491), \ + _PyLong_DIGIT_INIT(492), \ + _PyLong_DIGIT_INIT(493), \ + _PyLong_DIGIT_INIT(494), \ + _PyLong_DIGIT_INIT(495), \ + _PyLong_DIGIT_INIT(496), \ + _PyLong_DIGIT_INIT(497), \ + _PyLong_DIGIT_INIT(498), \ + _PyLong_DIGIT_INIT(499), \ + _PyLong_DIGIT_INIT(500), \ + _PyLong_DIGIT_INIT(501), \ + _PyLong_DIGIT_INIT(502), \ + _PyLong_DIGIT_INIT(503), \ + _PyLong_DIGIT_INIT(504), \ + _PyLong_DIGIT_INIT(505), \ + _PyLong_DIGIT_INIT(506), \ + _PyLong_DIGIT_INIT(507), \ + _PyLong_DIGIT_INIT(508), \ + _PyLong_DIGIT_INIT(509), \ + _PyLong_DIGIT_INIT(510), \ + _PyLong_DIGIT_INIT(511), \ + _PyLong_DIGIT_INIT(512), \ + _PyLong_DIGIT_INIT(513), \ + _PyLong_DIGIT_INIT(514), \ + _PyLong_DIGIT_INIT(515), \ + _PyLong_DIGIT_INIT(516), \ + _PyLong_DIGIT_INIT(517), \ + _PyLong_DIGIT_INIT(518), \ + _PyLong_DIGIT_INIT(519), \ + _PyLong_DIGIT_INIT(520), \ + _PyLong_DIGIT_INIT(521), \ + _PyLong_DIGIT_INIT(522), \ + _PyLong_DIGIT_INIT(523), \ + _PyLong_DIGIT_INIT(524), \ + _PyLong_DIGIT_INIT(525), \ + _PyLong_DIGIT_INIT(526), \ + _PyLong_DIGIT_INIT(527), \ + _PyLong_DIGIT_INIT(528), \ + _PyLong_DIGIT_INIT(529), \ + _PyLong_DIGIT_INIT(530), \ + _PyLong_DIGIT_INIT(531), \ + _PyLong_DIGIT_INIT(532), \ + _PyLong_DIGIT_INIT(533), \ + _PyLong_DIGIT_INIT(534), \ + _PyLong_DIGIT_INIT(535), \ + _PyLong_DIGIT_INIT(536), \ + _PyLong_DIGIT_INIT(537), \ + _PyLong_DIGIT_INIT(538), \ + _PyLong_DIGIT_INIT(539), \ + _PyLong_DIGIT_INIT(540), \ + _PyLong_DIGIT_INIT(541), \ + _PyLong_DIGIT_INIT(542), \ + _PyLong_DIGIT_INIT(543), \ + _PyLong_DIGIT_INIT(544), \ + _PyLong_DIGIT_INIT(545), \ + _PyLong_DIGIT_INIT(546), \ + _PyLong_DIGIT_INIT(547), \ + _PyLong_DIGIT_INIT(548), \ + _PyLong_DIGIT_INIT(549), \ + _PyLong_DIGIT_INIT(550), \ + _PyLong_DIGIT_INIT(551), \ + _PyLong_DIGIT_INIT(552), \ + _PyLong_DIGIT_INIT(553), \ + _PyLong_DIGIT_INIT(554), \ + _PyLong_DIGIT_INIT(555), \ + _PyLong_DIGIT_INIT(556), \ + _PyLong_DIGIT_INIT(557), \ + _PyLong_DIGIT_INIT(558), \ + _PyLong_DIGIT_INIT(559), \ + _PyLong_DIGIT_INIT(560), \ + _PyLong_DIGIT_INIT(561), \ + _PyLong_DIGIT_INIT(562), \ + _PyLong_DIGIT_INIT(563), \ + _PyLong_DIGIT_INIT(564), \ + _PyLong_DIGIT_INIT(565), \ + _PyLong_DIGIT_INIT(566), \ + _PyLong_DIGIT_INIT(567), \ + _PyLong_DIGIT_INIT(568), \ + _PyLong_DIGIT_INIT(569), \ + _PyLong_DIGIT_INIT(570), \ + _PyLong_DIGIT_INIT(571), \ + _PyLong_DIGIT_INIT(572), \ + _PyLong_DIGIT_INIT(573), \ + _PyLong_DIGIT_INIT(574), \ + _PyLong_DIGIT_INIT(575), \ + _PyLong_DIGIT_INIT(576), \ + _PyLong_DIGIT_INIT(577), \ + _PyLong_DIGIT_INIT(578), \ + _PyLong_DIGIT_INIT(579), \ + _PyLong_DIGIT_INIT(580), \ + _PyLong_DIGIT_INIT(581), \ + _PyLong_DIGIT_INIT(582), \ + _PyLong_DIGIT_INIT(583), \ + _PyLong_DIGIT_INIT(584), \ + _PyLong_DIGIT_INIT(585), \ + _PyLong_DIGIT_INIT(586), \ + _PyLong_DIGIT_INIT(587), \ + _PyLong_DIGIT_INIT(588), \ + _PyLong_DIGIT_INIT(589), \ + _PyLong_DIGIT_INIT(590), \ + _PyLong_DIGIT_INIT(591), \ + _PyLong_DIGIT_INIT(592), \ + _PyLong_DIGIT_INIT(593), \ + _PyLong_DIGIT_INIT(594), \ + _PyLong_DIGIT_INIT(595), \ + _PyLong_DIGIT_INIT(596), \ + _PyLong_DIGIT_INIT(597), \ + _PyLong_DIGIT_INIT(598), \ + _PyLong_DIGIT_INIT(599), \ + _PyLong_DIGIT_INIT(600), \ + _PyLong_DIGIT_INIT(601), \ + _PyLong_DIGIT_INIT(602), \ + _PyLong_DIGIT_INIT(603), \ + _PyLong_DIGIT_INIT(604), \ + _PyLong_DIGIT_INIT(605), \ + _PyLong_DIGIT_INIT(606), \ + _PyLong_DIGIT_INIT(607), \ + _PyLong_DIGIT_INIT(608), \ + _PyLong_DIGIT_INIT(609), \ + _PyLong_DIGIT_INIT(610), \ + _PyLong_DIGIT_INIT(611), \ + _PyLong_DIGIT_INIT(612), \ + _PyLong_DIGIT_INIT(613), \ + _PyLong_DIGIT_INIT(614), \ + _PyLong_DIGIT_INIT(615), \ + _PyLong_DIGIT_INIT(616), \ + _PyLong_DIGIT_INIT(617), \ + _PyLong_DIGIT_INIT(618), \ + _PyLong_DIGIT_INIT(619), \ + _PyLong_DIGIT_INIT(620), \ + _PyLong_DIGIT_INIT(621), \ + _PyLong_DIGIT_INIT(622), \ + _PyLong_DIGIT_INIT(623), \ + _PyLong_DIGIT_INIT(624), \ + _PyLong_DIGIT_INIT(625), \ + _PyLong_DIGIT_INIT(626), \ + _PyLong_DIGIT_INIT(627), \ + _PyLong_DIGIT_INIT(628), \ + _PyLong_DIGIT_INIT(629), \ + _PyLong_DIGIT_INIT(630), \ + _PyLong_DIGIT_INIT(631), \ + _PyLong_DIGIT_INIT(632), \ + _PyLong_DIGIT_INIT(633), \ + _PyLong_DIGIT_INIT(634), \ + _PyLong_DIGIT_INIT(635), \ + _PyLong_DIGIT_INIT(636), \ + _PyLong_DIGIT_INIT(637), \ + _PyLong_DIGIT_INIT(638), \ + _PyLong_DIGIT_INIT(639), \ + _PyLong_DIGIT_INIT(640), \ + _PyLong_DIGIT_INIT(641), \ + _PyLong_DIGIT_INIT(642), \ + _PyLong_DIGIT_INIT(643), \ + _PyLong_DIGIT_INIT(644), \ + _PyLong_DIGIT_INIT(645), \ + _PyLong_DIGIT_INIT(646), \ + _PyLong_DIGIT_INIT(647), \ + _PyLong_DIGIT_INIT(648), \ + _PyLong_DIGIT_INIT(649), \ + _PyLong_DIGIT_INIT(650), \ + _PyLong_DIGIT_INIT(651), \ + _PyLong_DIGIT_INIT(652), \ + _PyLong_DIGIT_INIT(653), \ + _PyLong_DIGIT_INIT(654), \ + _PyLong_DIGIT_INIT(655), \ + _PyLong_DIGIT_INIT(656), \ + _PyLong_DIGIT_INIT(657), \ + _PyLong_DIGIT_INIT(658), \ + _PyLong_DIGIT_INIT(659), \ + _PyLong_DIGIT_INIT(660), \ + _PyLong_DIGIT_INIT(661), \ + _PyLong_DIGIT_INIT(662), \ + _PyLong_DIGIT_INIT(663), \ + _PyLong_DIGIT_INIT(664), \ + _PyLong_DIGIT_INIT(665), \ + _PyLong_DIGIT_INIT(666), \ + _PyLong_DIGIT_INIT(667), \ + _PyLong_DIGIT_INIT(668), \ + _PyLong_DIGIT_INIT(669), \ + _PyLong_DIGIT_INIT(670), \ + _PyLong_DIGIT_INIT(671), \ + _PyLong_DIGIT_INIT(672), \ + _PyLong_DIGIT_INIT(673), \ + _PyLong_DIGIT_INIT(674), \ + _PyLong_DIGIT_INIT(675), \ + _PyLong_DIGIT_INIT(676), \ + _PyLong_DIGIT_INIT(677), \ + _PyLong_DIGIT_INIT(678), \ + _PyLong_DIGIT_INIT(679), \ + _PyLong_DIGIT_INIT(680), \ + _PyLong_DIGIT_INIT(681), \ + _PyLong_DIGIT_INIT(682), \ + _PyLong_DIGIT_INIT(683), \ + _PyLong_DIGIT_INIT(684), \ + _PyLong_DIGIT_INIT(685), \ + _PyLong_DIGIT_INIT(686), \ + _PyLong_DIGIT_INIT(687), \ + _PyLong_DIGIT_INIT(688), \ + _PyLong_DIGIT_INIT(689), \ + _PyLong_DIGIT_INIT(690), \ + _PyLong_DIGIT_INIT(691), \ + _PyLong_DIGIT_INIT(692), \ + _PyLong_DIGIT_INIT(693), \ + _PyLong_DIGIT_INIT(694), \ + _PyLong_DIGIT_INIT(695), \ + _PyLong_DIGIT_INIT(696), \ + _PyLong_DIGIT_INIT(697), \ + _PyLong_DIGIT_INIT(698), \ + _PyLong_DIGIT_INIT(699), \ + _PyLong_DIGIT_INIT(700), \ + _PyLong_DIGIT_INIT(701), \ + _PyLong_DIGIT_INIT(702), \ + _PyLong_DIGIT_INIT(703), \ + _PyLong_DIGIT_INIT(704), \ + _PyLong_DIGIT_INIT(705), \ + _PyLong_DIGIT_INIT(706), \ + _PyLong_DIGIT_INIT(707), \ + _PyLong_DIGIT_INIT(708), \ + _PyLong_DIGIT_INIT(709), \ + _PyLong_DIGIT_INIT(710), \ + _PyLong_DIGIT_INIT(711), \ + _PyLong_DIGIT_INIT(712), \ + _PyLong_DIGIT_INIT(713), \ + _PyLong_DIGIT_INIT(714), \ + _PyLong_DIGIT_INIT(715), \ + _PyLong_DIGIT_INIT(716), \ + _PyLong_DIGIT_INIT(717), \ + _PyLong_DIGIT_INIT(718), \ + _PyLong_DIGIT_INIT(719), \ + _PyLong_DIGIT_INIT(720), \ + _PyLong_DIGIT_INIT(721), \ + _PyLong_DIGIT_INIT(722), \ + _PyLong_DIGIT_INIT(723), \ + _PyLong_DIGIT_INIT(724), \ + _PyLong_DIGIT_INIT(725), \ + _PyLong_DIGIT_INIT(726), \ + _PyLong_DIGIT_INIT(727), \ + _PyLong_DIGIT_INIT(728), \ + _PyLong_DIGIT_INIT(729), \ + _PyLong_DIGIT_INIT(730), \ + _PyLong_DIGIT_INIT(731), \ + _PyLong_DIGIT_INIT(732), \ + _PyLong_DIGIT_INIT(733), \ + _PyLong_DIGIT_INIT(734), \ + _PyLong_DIGIT_INIT(735), \ + _PyLong_DIGIT_INIT(736), \ + _PyLong_DIGIT_INIT(737), \ + _PyLong_DIGIT_INIT(738), \ + _PyLong_DIGIT_INIT(739), \ + _PyLong_DIGIT_INIT(740), \ + _PyLong_DIGIT_INIT(741), \ + _PyLong_DIGIT_INIT(742), \ + _PyLong_DIGIT_INIT(743), \ + _PyLong_DIGIT_INIT(744), \ + _PyLong_DIGIT_INIT(745), \ + _PyLong_DIGIT_INIT(746), \ + _PyLong_DIGIT_INIT(747), \ + _PyLong_DIGIT_INIT(748), \ + _PyLong_DIGIT_INIT(749), \ + _PyLong_DIGIT_INIT(750), \ + _PyLong_DIGIT_INIT(751), \ + _PyLong_DIGIT_INIT(752), \ + _PyLong_DIGIT_INIT(753), \ + _PyLong_DIGIT_INIT(754), \ + _PyLong_DIGIT_INIT(755), \ + _PyLong_DIGIT_INIT(756), \ + _PyLong_DIGIT_INIT(757), \ + _PyLong_DIGIT_INIT(758), \ + _PyLong_DIGIT_INIT(759), \ + _PyLong_DIGIT_INIT(760), \ + _PyLong_DIGIT_INIT(761), \ + _PyLong_DIGIT_INIT(762), \ + _PyLong_DIGIT_INIT(763), \ + _PyLong_DIGIT_INIT(764), \ + _PyLong_DIGIT_INIT(765), \ + _PyLong_DIGIT_INIT(766), \ + _PyLong_DIGIT_INIT(767), \ + _PyLong_DIGIT_INIT(768), \ + _PyLong_DIGIT_INIT(769), \ + _PyLong_DIGIT_INIT(770), \ + _PyLong_DIGIT_INIT(771), \ + _PyLong_DIGIT_INIT(772), \ + _PyLong_DIGIT_INIT(773), \ + _PyLong_DIGIT_INIT(774), \ + _PyLong_DIGIT_INIT(775), \ + _PyLong_DIGIT_INIT(776), \ + _PyLong_DIGIT_INIT(777), \ + _PyLong_DIGIT_INIT(778), \ + _PyLong_DIGIT_INIT(779), \ + _PyLong_DIGIT_INIT(780), \ + _PyLong_DIGIT_INIT(781), \ + _PyLong_DIGIT_INIT(782), \ + _PyLong_DIGIT_INIT(783), \ + _PyLong_DIGIT_INIT(784), \ + _PyLong_DIGIT_INIT(785), \ + _PyLong_DIGIT_INIT(786), \ + _PyLong_DIGIT_INIT(787), \ + _PyLong_DIGIT_INIT(788), \ + _PyLong_DIGIT_INIT(789), \ + _PyLong_DIGIT_INIT(790), \ + _PyLong_DIGIT_INIT(791), \ + _PyLong_DIGIT_INIT(792), \ + _PyLong_DIGIT_INIT(793), \ + _PyLong_DIGIT_INIT(794), \ + _PyLong_DIGIT_INIT(795), \ + _PyLong_DIGIT_INIT(796), \ + _PyLong_DIGIT_INIT(797), \ + _PyLong_DIGIT_INIT(798), \ + _PyLong_DIGIT_INIT(799), \ + _PyLong_DIGIT_INIT(800), \ + _PyLong_DIGIT_INIT(801), \ + _PyLong_DIGIT_INIT(802), \ + _PyLong_DIGIT_INIT(803), \ + _PyLong_DIGIT_INIT(804), \ + _PyLong_DIGIT_INIT(805), \ + _PyLong_DIGIT_INIT(806), \ + _PyLong_DIGIT_INIT(807), \ + _PyLong_DIGIT_INIT(808), \ + _PyLong_DIGIT_INIT(809), \ + _PyLong_DIGIT_INIT(810), \ + _PyLong_DIGIT_INIT(811), \ + _PyLong_DIGIT_INIT(812), \ + _PyLong_DIGIT_INIT(813), \ + _PyLong_DIGIT_INIT(814), \ + _PyLong_DIGIT_INIT(815), \ + _PyLong_DIGIT_INIT(816), \ + _PyLong_DIGIT_INIT(817), \ + _PyLong_DIGIT_INIT(818), \ + _PyLong_DIGIT_INIT(819), \ + _PyLong_DIGIT_INIT(820), \ + _PyLong_DIGIT_INIT(821), \ + _PyLong_DIGIT_INIT(822), \ + _PyLong_DIGIT_INIT(823), \ + _PyLong_DIGIT_INIT(824), \ + _PyLong_DIGIT_INIT(825), \ + _PyLong_DIGIT_INIT(826), \ + _PyLong_DIGIT_INIT(827), \ + _PyLong_DIGIT_INIT(828), \ + _PyLong_DIGIT_INIT(829), \ + _PyLong_DIGIT_INIT(830), \ + _PyLong_DIGIT_INIT(831), \ + _PyLong_DIGIT_INIT(832), \ + _PyLong_DIGIT_INIT(833), \ + _PyLong_DIGIT_INIT(834), \ + _PyLong_DIGIT_INIT(835), \ + _PyLong_DIGIT_INIT(836), \ + _PyLong_DIGIT_INIT(837), \ + _PyLong_DIGIT_INIT(838), \ + _PyLong_DIGIT_INIT(839), \ + _PyLong_DIGIT_INIT(840), \ + _PyLong_DIGIT_INIT(841), \ + _PyLong_DIGIT_INIT(842), \ + _PyLong_DIGIT_INIT(843), \ + _PyLong_DIGIT_INIT(844), \ + _PyLong_DIGIT_INIT(845), \ + _PyLong_DIGIT_INIT(846), \ + _PyLong_DIGIT_INIT(847), \ + _PyLong_DIGIT_INIT(848), \ + _PyLong_DIGIT_INIT(849), \ + _PyLong_DIGIT_INIT(850), \ + _PyLong_DIGIT_INIT(851), \ + _PyLong_DIGIT_INIT(852), \ + _PyLong_DIGIT_INIT(853), \ + _PyLong_DIGIT_INIT(854), \ + _PyLong_DIGIT_INIT(855), \ + _PyLong_DIGIT_INIT(856), \ + _PyLong_DIGIT_INIT(857), \ + _PyLong_DIGIT_INIT(858), \ + _PyLong_DIGIT_INIT(859), \ + _PyLong_DIGIT_INIT(860), \ + _PyLong_DIGIT_INIT(861), \ + _PyLong_DIGIT_INIT(862), \ + _PyLong_DIGIT_INIT(863), \ + _PyLong_DIGIT_INIT(864), \ + _PyLong_DIGIT_INIT(865), \ + _PyLong_DIGIT_INIT(866), \ + _PyLong_DIGIT_INIT(867), \ + _PyLong_DIGIT_INIT(868), \ + _PyLong_DIGIT_INIT(869), \ + _PyLong_DIGIT_INIT(870), \ + _PyLong_DIGIT_INIT(871), \ + _PyLong_DIGIT_INIT(872), \ + _PyLong_DIGIT_INIT(873), \ + _PyLong_DIGIT_INIT(874), \ + _PyLong_DIGIT_INIT(875), \ + _PyLong_DIGIT_INIT(876), \ + _PyLong_DIGIT_INIT(877), \ + _PyLong_DIGIT_INIT(878), \ + _PyLong_DIGIT_INIT(879), \ + _PyLong_DIGIT_INIT(880), \ + _PyLong_DIGIT_INIT(881), \ + _PyLong_DIGIT_INIT(882), \ + _PyLong_DIGIT_INIT(883), \ + _PyLong_DIGIT_INIT(884), \ + _PyLong_DIGIT_INIT(885), \ + _PyLong_DIGIT_INIT(886), \ + _PyLong_DIGIT_INIT(887), \ + _PyLong_DIGIT_INIT(888), \ + _PyLong_DIGIT_INIT(889), \ + _PyLong_DIGIT_INIT(890), \ + _PyLong_DIGIT_INIT(891), \ + _PyLong_DIGIT_INIT(892), \ + _PyLong_DIGIT_INIT(893), \ + _PyLong_DIGIT_INIT(894), \ + _PyLong_DIGIT_INIT(895), \ + _PyLong_DIGIT_INIT(896), \ + _PyLong_DIGIT_INIT(897), \ + _PyLong_DIGIT_INIT(898), \ + _PyLong_DIGIT_INIT(899), \ + _PyLong_DIGIT_INIT(900), \ + _PyLong_DIGIT_INIT(901), \ + _PyLong_DIGIT_INIT(902), \ + _PyLong_DIGIT_INIT(903), \ + _PyLong_DIGIT_INIT(904), \ + _PyLong_DIGIT_INIT(905), \ + _PyLong_DIGIT_INIT(906), \ + _PyLong_DIGIT_INIT(907), \ + _PyLong_DIGIT_INIT(908), \ + _PyLong_DIGIT_INIT(909), \ + _PyLong_DIGIT_INIT(910), \ + _PyLong_DIGIT_INIT(911), \ + _PyLong_DIGIT_INIT(912), \ + _PyLong_DIGIT_INIT(913), \ + _PyLong_DIGIT_INIT(914), \ + _PyLong_DIGIT_INIT(915), \ + _PyLong_DIGIT_INIT(916), \ + _PyLong_DIGIT_INIT(917), \ + _PyLong_DIGIT_INIT(918), \ + _PyLong_DIGIT_INIT(919), \ + _PyLong_DIGIT_INIT(920), \ + _PyLong_DIGIT_INIT(921), \ + _PyLong_DIGIT_INIT(922), \ + _PyLong_DIGIT_INIT(923), \ + _PyLong_DIGIT_INIT(924), \ + _PyLong_DIGIT_INIT(925), \ + _PyLong_DIGIT_INIT(926), \ + _PyLong_DIGIT_INIT(927), \ + _PyLong_DIGIT_INIT(928), \ + _PyLong_DIGIT_INIT(929), \ + _PyLong_DIGIT_INIT(930), \ + _PyLong_DIGIT_INIT(931), \ + _PyLong_DIGIT_INIT(932), \ + _PyLong_DIGIT_INIT(933), \ + _PyLong_DIGIT_INIT(934), \ + _PyLong_DIGIT_INIT(935), \ + _PyLong_DIGIT_INIT(936), \ + _PyLong_DIGIT_INIT(937), \ + _PyLong_DIGIT_INIT(938), \ + _PyLong_DIGIT_INIT(939), \ + _PyLong_DIGIT_INIT(940), \ + _PyLong_DIGIT_INIT(941), \ + _PyLong_DIGIT_INIT(942), \ + _PyLong_DIGIT_INIT(943), \ + _PyLong_DIGIT_INIT(944), \ + _PyLong_DIGIT_INIT(945), \ + _PyLong_DIGIT_INIT(946), \ + _PyLong_DIGIT_INIT(947), \ + _PyLong_DIGIT_INIT(948), \ + _PyLong_DIGIT_INIT(949), \ + _PyLong_DIGIT_INIT(950), \ + _PyLong_DIGIT_INIT(951), \ + _PyLong_DIGIT_INIT(952), \ + _PyLong_DIGIT_INIT(953), \ + _PyLong_DIGIT_INIT(954), \ + _PyLong_DIGIT_INIT(955), \ + _PyLong_DIGIT_INIT(956), \ + _PyLong_DIGIT_INIT(957), \ + _PyLong_DIGIT_INIT(958), \ + _PyLong_DIGIT_INIT(959), \ + _PyLong_DIGIT_INIT(960), \ + _PyLong_DIGIT_INIT(961), \ + _PyLong_DIGIT_INIT(962), \ + _PyLong_DIGIT_INIT(963), \ + _PyLong_DIGIT_INIT(964), \ + _PyLong_DIGIT_INIT(965), \ + _PyLong_DIGIT_INIT(966), \ + _PyLong_DIGIT_INIT(967), \ + _PyLong_DIGIT_INIT(968), \ + _PyLong_DIGIT_INIT(969), \ + _PyLong_DIGIT_INIT(970), \ + _PyLong_DIGIT_INIT(971), \ + _PyLong_DIGIT_INIT(972), \ + _PyLong_DIGIT_INIT(973), \ + _PyLong_DIGIT_INIT(974), \ + _PyLong_DIGIT_INIT(975), \ + _PyLong_DIGIT_INIT(976), \ + _PyLong_DIGIT_INIT(977), \ + _PyLong_DIGIT_INIT(978), \ + _PyLong_DIGIT_INIT(979), \ + _PyLong_DIGIT_INIT(980), \ + _PyLong_DIGIT_INIT(981), \ + _PyLong_DIGIT_INIT(982), \ + _PyLong_DIGIT_INIT(983), \ + _PyLong_DIGIT_INIT(984), \ + _PyLong_DIGIT_INIT(985), \ + _PyLong_DIGIT_INIT(986), \ + _PyLong_DIGIT_INIT(987), \ + _PyLong_DIGIT_INIT(988), \ + _PyLong_DIGIT_INIT(989), \ + _PyLong_DIGIT_INIT(990), \ + _PyLong_DIGIT_INIT(991), \ + _PyLong_DIGIT_INIT(992), \ + _PyLong_DIGIT_INIT(993), \ + _PyLong_DIGIT_INIT(994), \ + _PyLong_DIGIT_INIT(995), \ + _PyLong_DIGIT_INIT(996), \ + _PyLong_DIGIT_INIT(997), \ + _PyLong_DIGIT_INIT(998), \ + _PyLong_DIGIT_INIT(999), \ + _PyLong_DIGIT_INIT(1000), \ + _PyLong_DIGIT_INIT(1001), \ + _PyLong_DIGIT_INIT(1002), \ + _PyLong_DIGIT_INIT(1003), \ + _PyLong_DIGIT_INIT(1004), \ + _PyLong_DIGIT_INIT(1005), \ + _PyLong_DIGIT_INIT(1006), \ + _PyLong_DIGIT_INIT(1007), \ + _PyLong_DIGIT_INIT(1008), \ + _PyLong_DIGIT_INIT(1009), \ + _PyLong_DIGIT_INIT(1010), \ + _PyLong_DIGIT_INIT(1011), \ + _PyLong_DIGIT_INIT(1012), \ + _PyLong_DIGIT_INIT(1013), \ + _PyLong_DIGIT_INIT(1014), \ + _PyLong_DIGIT_INIT(1015), \ + _PyLong_DIGIT_INIT(1016), \ + _PyLong_DIGIT_INIT(1017), \ + _PyLong_DIGIT_INIT(1018), \ + _PyLong_DIGIT_INIT(1019), \ + _PyLong_DIGIT_INIT(1020), \ + _PyLong_DIGIT_INIT(1021), \ + _PyLong_DIGIT_INIT(1022), \ + _PyLong_DIGIT_INIT(1023), \ + _PyLong_DIGIT_INIT(1024), \ } #define _Py_bytes_characters_INIT { \ @@ -553,19 +1321,35 @@ extern "C" { INIT_STR(dot_locals, "."), \ INIT_STR(empty, ""), \ INIT_STR(format, ".format"), \ + INIT_STR(gc, ""), \ INIT_STR(generic_base, ".generic_base"), \ INIT_STR(json_decoder, "json.decoder"), \ INIT_STR(kwdefaults, ".kwdefaults"), \ INIT_STR(list_err, "list index out of range"), \ + INIT_STR(native, ""), \ INIT_STR(str_replace_inf, "1e309"), \ INIT_STR(type_params, ".type_params"), \ INIT_STR(utf_8, "utf-8"), \ } #define _Py_str_identifiers_INIT { \ + INIT_ID(AGEN_CLOSED), \ + INIT_ID(AGEN_CREATED), \ + INIT_ID(AGEN_RUNNING), \ + INIT_ID(AGEN_SUSPENDED), \ INIT_ID(CANCELLED), \ + INIT_ID(CORO_CLOSED), \ + INIT_ID(CORO_CREATED), \ + INIT_ID(CORO_RUNNING), \ + INIT_ID(CORO_SUSPENDED), \ + INIT_ID(Emax), \ + INIT_ID(Emin), \ INIT_ID(FINISHED), \ INIT_ID(False), \ + INIT_ID(GEN_CLOSED), \ + INIT_ID(GEN_CREATED), \ + INIT_ID(GEN_RUNNING), \ + INIT_ID(GEN_SUSPENDED), \ INIT_ID(JSONDecodeError), \ INIT_ID(PENDING), \ INIT_ID(Py_Repr), \ @@ -660,6 +1444,8 @@ extern "C" { INIT_ID(__iter__), \ INIT_ID(__itruediv__), \ INIT_ID(__ixor__), \ + INIT_ID(__lazy_import__), \ + INIT_ID(__lazy_modules__), \ INIT_ID(__le__), \ INIT_ID(__len__), \ INIT_ID(__length_hint__), \ @@ -756,6 +1542,7 @@ extern "C" { INIT_ID(_filters), \ INIT_ID(_finalizing), \ INIT_ID(_find_and_load), \ + INIT_ID(_find_and_load_lazy_submodule), \ INIT_ID(_fix_up_module), \ INIT_ID(_flags_), \ INIT_ID(_get_sourcefile), \ @@ -788,14 +1575,16 @@ extern "C" { INIT_ID(aclose), \ INIT_ID(add), \ INIT_ID(add_done_callback), \ + INIT_ID(adobe), \ INIT_ID(after_in_child), \ INIT_ID(after_in_parent), \ - INIT_ID(aggregate_class), \ INIT_ID(alias), \ INIT_ID(align), \ INIT_ID(all), \ + INIT_ID(all_interpreters), \ INIT_ID(all_threads), \ INIT_ID(allow_code), \ + INIT_ID(alphabet), \ INIT_ID(any), \ INIT_ID(append), \ INIT_ID(arg), \ @@ -808,7 +1597,6 @@ extern "C" { INIT_ID(ast), \ INIT_ID(athrow), \ INIT_ID(attribute), \ - INIT_ID(authorizer_callback), \ INIT_ID(autocommit), \ INIT_ID(backtick), \ INIT_ID(base), \ @@ -818,6 +1606,7 @@ extern "C" { INIT_ID(bit_offset), \ INIT_ID(bit_size), \ INIT_ID(block), \ + INIT_ID(blocking), \ INIT_ID(bound), \ INIT_ID(buffer), \ INIT_ID(buffer_callback), \ @@ -835,6 +1624,8 @@ extern "C" { INIT_ID(c_exception), \ INIT_ID(c_parameter_type), \ INIT_ID(c_return), \ + INIT_ID(c_stack), \ + INIT_ID(cache_frames), \ INIT_ID(cached_datetime_module), \ INIT_ID(cached_statements), \ INIT_ID(cadata), \ @@ -842,13 +1633,19 @@ extern "C" { INIT_ID(call), \ INIT_ID(call_exception_handler), \ INIT_ID(call_soon), \ + INIT_ID(callable), \ INIT_ID(callback), \ INIT_ID(cancel), \ + INIT_ID(canonical), \ INIT_ID(capath), \ + INIT_ID(capitals), \ + INIT_ID(capture_features), \ INIT_ID(category), \ INIT_ID(cb_type), \ INIT_ID(certfile), \ + INIT_ID(chain), \ INIT_ID(check_same_thread), \ + INIT_ID(clamp), \ INIT_ID(clear), \ INIT_ID(close), \ INIT_ID(closed), \ @@ -874,9 +1671,12 @@ extern "C" { INIT_ID(co_varnames), \ INIT_ID(code), \ INIT_ID(col_offset), \ + INIT_ID(collector), \ INIT_ID(command), \ INIT_ID(comment_factory), \ INIT_ID(compile_mode), \ + INIT_ID(compression), \ + INIT_ID(config), \ INIT_ID(consts), \ INIT_ID(context), \ INIT_ID(contravariant), \ @@ -887,11 +1687,14 @@ extern "C" { INIT_ID(coro), \ INIT_ID(count), \ INIT_ID(covariant), \ + INIT_ID(ctx), \ INIT_ID(cwd), \ INIT_ID(d_parameter_type), \ INIT_ID(data), \ INIT_ID(database), \ + INIT_ID(date), \ INIT_ID(day), \ + INIT_ID(days), \ INIT_ID(debug), \ INIT_ID(decode), \ INIT_ID(decoder), \ @@ -904,7 +1707,6 @@ extern "C" { INIT_ID(deterministic), \ INIT_ID(device), \ INIT_ID(dict), \ - INIT_ID(dictcomp), \ INIT_ID(difference_update), \ INIT_ID(digest), \ INIT_ID(digest_size), \ @@ -919,9 +1721,11 @@ extern "C" { INIT_ID(dont_inherit), \ INIT_ID(dst), \ INIT_ID(dst_dir_fd), \ + INIT_ID(duration_sec), \ INIT_ID(eager_start), \ INIT_ID(effective_ids), \ INIT_ID(element_factory), \ + INIT_ID(emptyerror), \ INIT_ID(encode), \ INIT_ID(encoding), \ INIT_ID(end), \ @@ -931,20 +1735,26 @@ extern "C" { INIT_ID(endpos), \ INIT_ID(entrypoint), \ INIT_ID(env), \ + INIT_ID(error_rate), \ INIT_ID(errors), \ INIT_ID(event), \ INIT_ID(eventmask), \ + INIT_ID(exc), \ + INIT_ID(exc_tb), \ INIT_ID(exc_type), \ + INIT_ID(exc_val), \ INIT_ID(exc_value), \ INIT_ID(excepthook), \ INIT_ID(exception), \ INIT_ID(existing_file_name), \ + INIT_ID(exit), \ INIT_ID(exp), \ INIT_ID(expression), \ INIT_ID(extend), \ INIT_ID(extra_tokens), \ INIT_ID(facility), \ INIT_ID(factory), \ + INIT_ID(fallback), \ INIT_ID(false), \ INIT_ID(family), \ INIT_ID(fanout), \ @@ -967,19 +1777,22 @@ extern "C" { INIT_ID(flags), \ INIT_ID(flush), \ INIT_ID(fold), \ + INIT_ID(foldspaces), \ INIT_ID(follow_symlinks), \ INIT_ID(format), \ INIT_ID(format_spec), \ INIT_ID(frame_buffer), \ + INIT_ID(free_threaded), \ INIT_ID(from_param), \ INIT_ID(fromlist), \ INIT_ID(fromtimestamp), \ INIT_ID(fromutc), \ INIT_ID(fset), \ + INIT_ID(fullerror), \ INIT_ID(func), \ INIT_ID(future), \ + INIT_ID(gc), \ INIT_ID(generation), \ - INIT_ID(genexpr), \ INIT_ID(get), \ INIT_ID(get_debug), \ INIT_ID(get_event_loop), \ @@ -1000,13 +1813,17 @@ extern "C" { INIT_ID(hi), \ INIT_ID(hook), \ INIT_ID(hour), \ + INIT_ID(hours), \ INIT_ID(id), \ INIT_ID(ident), \ INIT_ID(identity_hint), \ INIT_ID(ignore), \ + INIT_ID(ignorechars), \ INIT_ID(imag), \ + INIT_ID(implieslink), \ INIT_ID(importlib), \ INIT_ID(in_fd), \ + INIT_ID(include_aliases), \ INIT_ID(incoming), \ INIT_ID(index), \ INIT_ID(indexgroup), \ @@ -1053,9 +1870,9 @@ extern "C" { INIT_ID(kw), \ INIT_ID(kw1), \ INIT_ID(kw2), \ + INIT_ID(kwargs), \ INIT_ID(kwdefaults), \ INIT_ID(label), \ - INIT_ID(lambda), \ INIT_ID(last), \ INIT_ID(last_exc), \ INIT_ID(last_node), \ @@ -1064,6 +1881,7 @@ extern "C" { INIT_ID(last_value), \ INIT_ID(latin1), \ INIT_ID(leaf_size), \ + INIT_ID(legacy), \ INIT_ID(len), \ INIT_ID(length), \ INIT_ID(level), \ @@ -1071,7 +1889,6 @@ extern "C" { INIT_ID(line), \ INIT_ID(line_buffering), \ INIT_ID(lineno), \ - INIT_ID(listcomp), \ INIT_ID(little), \ INIT_ID(lo), \ INIT_ID(locale), \ @@ -1080,12 +1897,15 @@ extern "C" { INIT_ID(loop), \ INIT_ID(manual_reset), \ INIT_ID(mapping), \ + INIT_ID(mask), \ INIT_ID(match), \ INIT_ID(max_length), \ + INIT_ID(max_threads), \ INIT_ID(maxdigits), \ INIT_ID(maxevents), \ INIT_ID(maxlen), \ INIT_ID(maxmem), \ + INIT_ID(maxsize), \ INIT_ID(maxsplit), \ INIT_ID(maxvalue), \ INIT_ID(memLevel), \ @@ -1095,19 +1915,22 @@ extern "C" { INIT_ID(metadata), \ INIT_ID(method), \ INIT_ID(microsecond), \ + INIT_ID(microseconds), \ INIT_ID(milliseconds), \ INIT_ID(minute), \ + INIT_ID(minutes), \ + INIT_ID(missed_samples), \ INIT_ID(mod), \ INIT_ID(mode), \ INIT_ID(module), \ INIT_ID(module_globals), \ INIT_ID(modules), \ + INIT_ID(modulo), \ INIT_ID(month), \ INIT_ID(mro), \ INIT_ID(msg), \ INIT_ID(mutex), \ INIT_ID(mycmp), \ - INIT_ID(n_arg), \ INIT_ID(n_fields), \ INIT_ID(n_sequence_fields), \ INIT_ID(n_unnamed_fields), \ @@ -1115,7 +1938,7 @@ extern "C" { INIT_ID(name_from), \ INIT_ID(namespace_separator), \ INIT_ID(namespaces), \ - INIT_ID(narg), \ + INIT_ID(native), \ INIT_ID(ndigits), \ INIT_ID(nested), \ INIT_ID(new_file_name), \ @@ -1142,6 +1965,7 @@ extern "C" { INIT_ID(only_keys), \ INIT_ID(oparg), \ INIT_ID(opcode), \ + INIT_ID(opcodes), \ INIT_ID(open), \ INIT_ID(opener), \ INIT_ID(operation), \ @@ -1149,11 +1973,14 @@ extern "C" { INIT_ID(options), \ INIT_ID(order), \ INIT_ID(origin), \ + INIT_ID(other), \ INIT_ID(out_fd), \ INIT_ID(outgoing), \ INIT_ID(outpath), \ INIT_ID(overlapped), \ INIT_ID(owner), \ + INIT_ID(pad), \ + INIT_ID(padded), \ INIT_ID(pages), \ INIT_ID(parameter), \ INIT_ID(parent), \ @@ -1166,21 +1993,27 @@ extern "C" { INIT_ID(person), \ INIT_ID(pi_factory), \ INIT_ID(pid), \ + INIT_ID(pointer_bits), \ INIT_ID(policy), \ INIT_ID(pos), \ INIT_ID(pos1), \ INIT_ID(pos2), \ INIT_ID(posix), \ + INIT_ID(prec), \ + INIT_ID(preserve_exc), \ INIT_ID(print_file_and_line), \ INIT_ID(priority), \ INIT_ID(progress), \ - INIT_ID(progress_handler), \ + INIT_ID(progress_callback), \ INIT_ID(progress_routine), \ INIT_ID(proto), \ INIT_ID(protocol), \ INIT_ID(ps1), \ INIT_ID(ps2), \ + INIT_ID(qid), \ + INIT_ID(qualname), \ INIT_ID(query), \ + INIT_ID(queuetype), \ INIT_ID(quotetabs), \ INIT_ID(raw), \ INIT_ID(read), \ @@ -1192,24 +2025,34 @@ extern "C" { INIT_ID(readline), \ INIT_ID(readonly), \ INIT_ID(real), \ + INIT_ID(recursive), \ INIT_ID(reducer_override), \ INIT_ID(registry), \ INIT_ID(rel_tol), \ INIT_ID(release), \ INIT_ID(reload), \ + INIT_ID(repeat), \ INIT_ID(repl), \ INIT_ID(replace), \ + INIT_ID(repr), \ + INIT_ID(reqrefs), \ + INIT_ID(require_ready), \ INIT_ID(reserved), \ INIT_ID(reset), \ INIT_ID(resetids), \ + INIT_ID(restrict), \ INIT_ID(return), \ INIT_ID(reverse), \ INIT_ID(reversed), \ + INIT_ID(rounding), \ INIT_ID(salt), \ + INIT_ID(sample_interval_us), \ + INIT_ID(sample_rate), \ INIT_ID(sched_priority), \ INIT_ID(scheduler), \ INIT_ID(script), \ INIT_ID(second), \ + INIT_ID(seconds), \ INIT_ID(security_attributes), \ INIT_ID(seek), \ INIT_ID(seekable), \ @@ -1221,18 +2064,21 @@ extern "C" { INIT_ID(server_hostname), \ INIT_ID(server_side), \ INIT_ID(session), \ - INIT_ID(setcomp), \ INIT_ID(setpgroup), \ INIT_ID(setsid), \ INIT_ID(setsigdef), \ INIT_ID(setsigmask), \ INIT_ID(setstate), \ INIT_ID(shape), \ + INIT_ID(shared), \ + INIT_ID(short), \ INIT_ID(show_cmd), \ INIT_ID(signed), \ + INIT_ID(signum), \ INIT_ID(size), \ INIT_ID(sizehint), \ INIT_ID(skip_file_prefixes), \ + INIT_ID(skip_non_matching_threads), \ INIT_ID(sleep), \ INIT_ID(sock), \ INIT_ID(sort), \ @@ -1241,9 +2087,12 @@ extern "C" { INIT_ID(spam), \ INIT_ID(src), \ INIT_ID(src_dir_fd), \ + INIT_ID(stack_frames), \ INIT_ID(stacklevel), \ INIT_ID(start), \ + INIT_ID(start_time_us), \ INIT_ID(statement), \ + INIT_ID(stats), \ INIT_ID(status), \ INIT_ID(stderr), \ INIT_ID(stdin), \ @@ -1261,6 +2110,7 @@ extern "C" { INIT_ID(symmetric_difference_update), \ INIT_ID(tabsize), \ INIT_ID(tag), \ + INIT_ID(take_bytes), \ INIT_ID(target), \ INIT_ID(target_is_directory), \ INIT_ID(task), \ @@ -1272,18 +2122,23 @@ extern "C" { INIT_ID(template), \ INIT_ID(term), \ INIT_ID(text), \ + INIT_ID(third), \ INIT_ID(threading), \ INIT_ID(throw), \ + INIT_ID(time), \ INIT_ID(timeout), \ INIT_ID(timer), \ INIT_ID(times), \ + INIT_ID(timespec), \ + INIT_ID(timestamp), \ + INIT_ID(timestamp_us), \ INIT_ID(timetuple), \ INIT_ID(timeunit), \ INIT_ID(top), \ - INIT_ID(trace_callback), \ INIT_ID(traceback), \ INIT_ID(trailers), \ INIT_ID(translate), \ + INIT_ID(traps), \ INIT_ID(true), \ INIT_ID(truncate), \ INIT_ID(twice), \ @@ -1294,8 +2149,10 @@ extern "C" { INIT_ID(tzinfo), \ INIT_ID(tzname), \ INIT_ID(uid), \ + INIT_ID(unboundop), \ INIT_ID(unlink), \ INIT_ID(unraisablehook), \ + INIT_ID(updates), \ INIT_ID(uri), \ INIT_ID(usedforsecurity), \ INIT_ID(value), \ @@ -1309,9 +2166,11 @@ extern "C" { INIT_ID(wbits), \ INIT_ID(week), \ INIT_ID(weekday), \ + INIT_ID(weeks), \ INIT_ID(which), \ INIT_ID(who), \ INIT_ID(withdata), \ + INIT_ID(wrapcol), \ INIT_ID(writable), \ INIT_ID(write), \ INIT_ID(write_through), \ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_structs.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_structs.h index 78f236a51..145e66de9 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_structs.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_runtime_structs.h @@ -31,20 +31,10 @@ struct _pymem_allocators { debug_alloc_api_t obj; } debug; int is_debug_enabled; + int use_hugepages; PyObjectArenaAllocator obj_arena; }; -enum _py_float_format_type { - _py_float_format_unknown, - _py_float_format_ieee_big_endian, - _py_float_format_ieee_little_endian, -}; - -struct _Py_float_runtime_state { - enum _py_float_format_type float_format; - enum _py_float_format_type double_format; -}; - struct pyhash_runtime_state { struct { #ifndef MS_WINDOWS @@ -77,9 +67,7 @@ struct _fileutils_state { struct _parser_runtime_state { #ifdef Py_DEBUG long memo_statistics[_PYPEGEN_NSTATISTICS]; -#ifdef Py_GIL_DISABLED PyMutex mutex; -#endif #else int _not_used; #endif @@ -106,7 +94,7 @@ struct _Py_cached_objects { }; // These would be in pycore_long.h if it weren't for an include cycle. -#define _PY_NSMALLPOSINTS 257 +#define _PY_NSMALLPOSINTS 1025 #define _PY_NSMALLNEGINTS 5 #include "pycore_global_strings.h" // struct _Py_global_strings @@ -170,10 +158,18 @@ struct pyruntimestate { /* Is Python preinitialized? Set to 1 by Py_PreInitialize() */ int preinitialized; - /* Is Python core initialized? Set to 1 by _Py_InitializeCore() */ + /* Is Python core initialized? Set to 1 by _Py_InitializeCore(). + + Use _PyRuntimeState_GetCoreInitialized() and + _PyRuntimeState_SetCoreInitialized() to access it, + don't access it directly. */ int core_initialized; - /* Is Python fully initialized? Set to 1 by Py_Initialize() */ + /* Is Python fully initialized? Set to 1 by Py_Initialize(). + + Use _PyRuntimeState_GetInitialized() and + _PyRuntimeState_SetInitialized() to access it, + don't access it directly. */ int initialized; /* Set by Py_FinalizeEx(). Only reset to NULL if Py_Initialize() @@ -223,9 +219,6 @@ struct pyruntimestate { struct _pythread_runtime_state threads; struct _signals_runtime_state signals; - /* Used for the thread state bound to the current thread. */ - Py_tss_t autoTSSkey; - /* Used instead of PyThreadState.trash when there is not current tstate. */ Py_tss_t trashTSSkey; @@ -274,7 +267,6 @@ struct pyruntimestate { } audit_hooks; struct _py_object_runtime_state object_state; - struct _Py_float_runtime_state float_state; struct _Py_unicode_runtime_state unicode_state; struct _types_runtime_state types; struct _Py_time_runtime_state time; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_semaphore.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_semaphore.h index 269538384..66b4939dc 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_semaphore.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_semaphore.h @@ -46,10 +46,8 @@ typedef struct _PySemaphore { } _PySemaphore; // Puts the current thread to sleep until _PySemaphore_Wakeup() is called. -// If `detach` is true, then the thread will detach/release the GIL while -// sleeping. PyAPI_FUNC(int) -_PySemaphore_Wait(_PySemaphore *sema, PyTime_t timeout_ns, int detach); +_PySemaphore_Wait(_PySemaphore *sema, PyTime_t timeout_ns); // Wakes up a single thread waiting on sema. Note that _PySemaphore_Wakeup() // can be called before _PySemaphore_Wait(). diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_slots.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_slots.h new file mode 100644 index 000000000..960a632ae --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_slots.h @@ -0,0 +1,138 @@ +#ifndef _Py_PYCORE_SLOTS_H +#define _Py_PYCORE_SLOTS_H + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include + +/* Slot data type */ +typedef enum _PySlot_DTYPE { + _PySlot_DTYPE_VOID, + _PySlot_DTYPE_FUNC, + _PySlot_DTYPE_PTR, + _PySlot_DTYPE_SIZE, + _PySlot_DTYPE_INT64, + _PySlot_DTYPE_UINT64, +}_PySlot_DTYPE; + +/* Slot kind, used to identify: + * - the thing the slot initializes (type/module/special) + * - the struct type (PySlot/PyType_Slot/PyModuleDef_Slot) + */ +typedef enum _PySlot_KIND { + _PySlot_KIND_TYPE, + _PySlot_KIND_MOD, + _PySlot_KIND_COMPAT, + _PySlot_KIND_SLOT, +} _PySlot_KIND; + +typedef enum _PySlot_PROBLEM_HANDLING { + _PySlot_PROBLEM_ALLOW, + _PySlot_PROBLEM_DEPRECATED, + _PySlot_PROBLEM_REJECT, +} _PySlot_PROBLEM_HANDLING; + +PyAPI_DATA(const char *const) _PySlot_names[]; + +#define _PySlot_MAX_NESTING 5 + +/* State for one nesting level of a slots iterator */ +typedef struct _PySlotIterator_state { + union { + // tagged by slot_struct_kind: + const PySlot *slot; // with _PySlot_KIND_SLOT + const PyType_Slot *tp_slot; // with _PySlot_KIND_TYPE + const PyModuleDef_Slot *mod_slot; // with _PySlot_KIND_MOD + const void *any_slot; + }; + _PySlot_KIND slot_struct_kind; +} _PySlotIterator_state; + +#define _PySlot_SEEN_ENTRY_BITS (8 * sizeof(unsigned int)) + +/* State for a slots iterator */ +typedef struct { + _PySlotIterator_state *state; + _PySlotIterator_state states[_PySlot_MAX_NESTING]; + unsigned int seen[_Py_slot_COUNT / _PySlot_SEEN_ENTRY_BITS + 1]; + _PySlot_KIND kind; + uint8_t recursion_level; + bool is_at_end :1; + bool is_first_run :1; + + // Name of the object (type/module) being defined, NULL if unknown. + // Must be set by the callers as soon as it's known. + const char *name; + + /* Output information: */ + + // The slot. Always a copy; may be modified by caller of the iterator. + PySlot current; + +} _PySlotIterator; + +/* Initialize an iterator using a PySlot array */ +PyAPI_FUNC(void) +_PySlotIterator_Init(_PySlotIterator *it, const PySlot *slots, + _PySlot_KIND result_kind); + +/* Initialize an iterator using a legacy slot array */ +PyAPI_FUNC(void) +_PySlotIterator_InitLegacy(_PySlotIterator *it, const void *slots, + _PySlot_KIND kind); + +/* Reset a *successfully exhausted* iterator to the beginning. + * The *slots* must be the same as for the previous + * `_PySlotIterator_InitWithKind` call. + * (Unlike creating a new iterator, we can skip some validation after Rewind.) + */ +PyAPI_FUNC(void) _PySlotIterator_Rewind(_PySlotIterator *it, const void *slots); + +/* Iteration function. + * + * Return false at the end (when successfully exhausted). + * Otherwise (even on error), fill output information in `it` and return true. + * + * On error, set an exception and set `it->current.sl_id` to `Py_slot_invalid`. + */ +PyAPI_FUNC(bool) _PySlotIterator_Next(_PySlotIterator *it); + +/* Return 1 if given slot was "seen" by an earlier _PySlotIterator_Next call. + * (This state is not reset by rewinding.) + */ +PyAPI_FUNC(bool) _PySlotIterator_SawSlot(_PySlotIterator *, int); + +static inline const char * +_PySlot_GetName(uint16_t id) +{ + if (id >= _Py_slot_COUNT) { + return ""; + } + if (id == Py_slot_invalid) { + return "Py_slot_invalid"; + } + return _PySlot_names[id]; +} + +static inline void +_PySlot_err_bad_slot(char *kind, uint16_t id) +{ + if (id < _Py_slot_COUNT) { + PyErr_Format(PyExc_SystemError, "invalid %s slot %d (%s)", + kind, (unsigned int)id, _PySlot_names[id]); + } + else if (id == Py_slot_invalid) { + PyErr_Format(PyExc_SystemError, "invalid slot (Py_slot_invalid, %u)", + (unsigned int)id); + } + else { + PyErr_Format(PyExc_SystemError, "unknown %s slot ID %u", + kind, (unsigned int)id); + } +} + +#include "internal/pycore_slots_generated.h" + +#endif // _Py_PYCORE_SLOTS_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_slots_generated.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_slots_generated.h new file mode 100644 index 000000000..73a770700 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_slots_generated.h @@ -0,0 +1,958 @@ +/* Generated by Tools/build/generate_slots.py */ + +#ifndef _PY_HAVE_INTERNAL_SLOTS_GENERATED_H +#define _PY_HAVE_INTERNAL_SLOTS_GENERATED_H + +static inline uint16_t +_PySlot_resolve_type_slot(uint16_t slot_id) +{ + switch (slot_id) { + case 1: + return Py_bf_getbuffer; + case 2: + return Py_bf_releasebuffer; + case 3: + return Py_mp_ass_subscript; + case 4: + return Py_mp_length; + case Py_slot_end: + case Py_mp_subscript: + case Py_nb_absolute: + case Py_nb_add: + case Py_nb_and: + case Py_nb_bool: + case Py_nb_divmod: + case Py_nb_float: + case Py_nb_floor_divide: + case Py_nb_index: + case Py_nb_inplace_add: + case Py_nb_inplace_and: + case Py_nb_inplace_floor_divide: + case Py_nb_inplace_lshift: + case Py_nb_inplace_multiply: + case Py_nb_inplace_or: + case Py_nb_inplace_power: + case Py_nb_inplace_remainder: + case Py_nb_inplace_rshift: + case Py_nb_inplace_subtract: + case Py_nb_inplace_true_divide: + case Py_nb_inplace_xor: + case Py_nb_int: + case Py_nb_invert: + case Py_nb_lshift: + case Py_nb_multiply: + case Py_nb_negative: + case Py_nb_or: + case Py_nb_positive: + case Py_nb_power: + case Py_nb_remainder: + case Py_nb_rshift: + case Py_nb_subtract: + case Py_nb_true_divide: + case Py_nb_xor: + case Py_sq_ass_item: + case Py_sq_concat: + case Py_sq_contains: + case Py_sq_inplace_concat: + case Py_sq_inplace_repeat: + case Py_sq_item: + case Py_sq_length: + case Py_sq_repeat: + case Py_tp_alloc: + case Py_tp_base: + case Py_tp_bases: + case Py_tp_call: + case Py_tp_clear: + case Py_tp_dealloc: + case Py_tp_del: + case Py_tp_descr_get: + case Py_tp_descr_set: + case Py_tp_doc: + case Py_tp_getattr: + case Py_tp_getattro: + case Py_tp_hash: + case Py_tp_init: + case Py_tp_is_gc: + case Py_tp_iter: + case Py_tp_iternext: + case Py_tp_methods: + case Py_tp_new: + case Py_tp_repr: + case Py_tp_richcompare: + case Py_tp_setattr: + case Py_tp_setattro: + case Py_tp_str: + case Py_tp_traverse: + case Py_tp_members: + case Py_tp_getset: + case Py_tp_free: + case Py_nb_matrix_multiply: + case Py_nb_inplace_matrix_multiply: + case Py_am_await: + case Py_am_aiter: + case Py_am_anext: + case Py_tp_finalize: + case Py_am_send: + case Py_tp_vectorcall: + case Py_tp_token: + case Py_bf_getbuffer: + case Py_bf_releasebuffer: + case Py_mp_ass_subscript: + case Py_mp_length: + case Py_slot_subslots: + case Py_tp_slots: + case Py_tp_name: + case Py_tp_basicsize: + case Py_tp_extra_basicsize: + case Py_tp_itemsize: + case Py_tp_flags: + case Py_tp_metaclass: + case Py_tp_module: + return slot_id; + default: + return Py_slot_invalid; + } +} + +static inline uint16_t +_PySlot_resolve_mod_slot(uint16_t slot_id) +{ + switch (slot_id) { + case 1: + return Py_mod_create; + case 2: + return Py_mod_exec; + case 3: + return Py_mod_multiple_interpreters; + case 4: + return Py_mod_gil; + case Py_slot_end: + case Py_mod_create: + case Py_mod_exec: + case Py_mod_multiple_interpreters: + case Py_mod_gil: + case Py_slot_subslots: + case Py_mod_slots: + case Py_mod_name: + case Py_mod_doc: + case Py_mod_state_size: + case Py_mod_methods: + case Py_mod_state_traverse: + case Py_mod_state_clear: + case Py_mod_state_free: + case Py_mod_abi: + case Py_mod_token: + return slot_id; + default: + return Py_slot_invalid; + } +} + +static inline void* +_PySlot_type_getslot(PyTypeObject *tp, uint16_t slot_id) +{ + switch (slot_id) { + case Py_mp_subscript: + if (!(tp->tp_as_mapping)) return NULL; + return (void*)tp->tp_as_mapping->mp_subscript; + case Py_nb_absolute: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_absolute; + case Py_nb_add: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_add; + case Py_nb_and: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_and; + case Py_nb_bool: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_bool; + case Py_nb_divmod: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_divmod; + case Py_nb_float: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_float; + case Py_nb_floor_divide: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_floor_divide; + case Py_nb_index: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_index; + case Py_nb_inplace_add: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_add; + case Py_nb_inplace_and: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_and; + case Py_nb_inplace_floor_divide: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_floor_divide; + case Py_nb_inplace_lshift: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_lshift; + case Py_nb_inplace_multiply: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_multiply; + case Py_nb_inplace_or: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_or; + case Py_nb_inplace_power: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_power; + case Py_nb_inplace_remainder: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_remainder; + case Py_nb_inplace_rshift: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_rshift; + case Py_nb_inplace_subtract: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_subtract; + case Py_nb_inplace_true_divide: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_true_divide; + case Py_nb_inplace_xor: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_xor; + case Py_nb_int: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_int; + case Py_nb_invert: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_invert; + case Py_nb_lshift: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_lshift; + case Py_nb_multiply: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_multiply; + case Py_nb_negative: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_negative; + case Py_nb_or: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_or; + case Py_nb_positive: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_positive; + case Py_nb_power: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_power; + case Py_nb_remainder: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_remainder; + case Py_nb_rshift: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_rshift; + case Py_nb_subtract: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_subtract; + case Py_nb_true_divide: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_true_divide; + case Py_nb_xor: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_xor; + case Py_sq_ass_item: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_ass_item; + case Py_sq_concat: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_concat; + case Py_sq_contains: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_contains; + case Py_sq_inplace_concat: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_inplace_concat; + case Py_sq_inplace_repeat: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_inplace_repeat; + case Py_sq_item: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_item; + case Py_sq_length: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_length; + case Py_sq_repeat: + if (!(tp->tp_as_sequence)) return NULL; + return (void*)tp->tp_as_sequence->sq_repeat; + case Py_tp_alloc: + return (void*)tp->tp_alloc; + case Py_tp_base: + return (void*)tp->tp_base; + case Py_tp_bases: + return (void*)tp->tp_bases; + case Py_tp_call: + return (void*)tp->tp_call; + case Py_tp_clear: + return (void*)tp->tp_clear; + case Py_tp_dealloc: + return (void*)tp->tp_dealloc; + case Py_tp_del: + return (void*)tp->tp_del; + case Py_tp_descr_get: + return (void*)tp->tp_descr_get; + case Py_tp_descr_set: + return (void*)tp->tp_descr_set; + case Py_tp_doc: + return (void*)tp->tp_doc; + case Py_tp_getattr: + return (void*)tp->tp_getattr; + case Py_tp_getattro: + return (void*)tp->tp_getattro; + case Py_tp_hash: + return (void*)tp->tp_hash; + case Py_tp_init: + return (void*)tp->tp_init; + case Py_tp_is_gc: + return (void*)tp->tp_is_gc; + case Py_tp_iter: + return (void*)tp->tp_iter; + case Py_tp_iternext: + return (void*)tp->tp_iternext; + case Py_tp_methods: + return (void*)tp->tp_methods; + case Py_tp_new: + return (void*)tp->tp_new; + case Py_tp_repr: + return (void*)tp->tp_repr; + case Py_tp_richcompare: + return (void*)tp->tp_richcompare; + case Py_tp_setattr: + return (void*)tp->tp_setattr; + case Py_tp_setattro: + return (void*)tp->tp_setattro; + case Py_tp_str: + return (void*)tp->tp_str; + case Py_tp_traverse: + return (void*)tp->tp_traverse; + case Py_tp_members: + return (void*)tp->tp_members; + case Py_tp_getset: + return (void*)tp->tp_getset; + case Py_tp_free: + return (void*)tp->tp_free; + case Py_nb_matrix_multiply: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_matrix_multiply; + case Py_nb_inplace_matrix_multiply: + if (!(tp->tp_as_number)) return NULL; + return (void*)tp->tp_as_number->nb_inplace_matrix_multiply; + case Py_am_await: + if (!(tp->tp_as_async)) return NULL; + return (void*)tp->tp_as_async->am_await; + case Py_am_aiter: + if (!(tp->tp_as_async)) return NULL; + return (void*)tp->tp_as_async->am_aiter; + case Py_am_anext: + if (!(tp->tp_as_async)) return NULL; + return (void*)tp->tp_as_async->am_anext; + case Py_tp_finalize: + return (void*)tp->tp_finalize; + case Py_am_send: + if (!(tp->tp_as_async)) return NULL; + return (void*)tp->tp_as_async->am_send; + case Py_tp_vectorcall: + return (void*)tp->tp_vectorcall; + case Py_tp_token: + if (!(tp->tp_flags & Py_TPFLAGS_HEAPTYPE)) return NULL; + return (void*)((PyHeapTypeObject*)tp)->ht_token; + case Py_bf_getbuffer: + if (!(tp->tp_as_buffer)) return NULL; + return (void*)tp->tp_as_buffer->bf_getbuffer; + case Py_bf_releasebuffer: + if (!(tp->tp_as_buffer)) return NULL; + return (void*)tp->tp_as_buffer->bf_releasebuffer; + case Py_mp_ass_subscript: + if (!(tp->tp_as_mapping)) return NULL; + return (void*)tp->tp_as_mapping->mp_ass_subscript; + case Py_mp_length: + if (!(tp->tp_as_mapping)) return NULL; + return (void*)tp->tp_as_mapping->mp_length; + } + _PySlot_err_bad_slot("PyType_GetSlot", slot_id); + return NULL; +} + +static inline void +_PySlot_heaptype_apply_field_slot(PyHeapTypeObject *ht, PySlot slot) +{ + switch (slot.sl_id) { + case Py_mp_subscript: + ht->as_mapping.mp_subscript = (binaryfunc)slot.sl_func; + break; + case Py_nb_absolute: + ht->as_number.nb_absolute = (unaryfunc)slot.sl_func; + break; + case Py_nb_add: + ht->as_number.nb_add = (binaryfunc)slot.sl_func; + break; + case Py_nb_and: + ht->as_number.nb_and = (binaryfunc)slot.sl_func; + break; + case Py_nb_bool: + ht->as_number.nb_bool = (inquiry)slot.sl_func; + break; + case Py_nb_divmod: + ht->as_number.nb_divmod = (binaryfunc)slot.sl_func; + break; + case Py_nb_float: + ht->as_number.nb_float = (unaryfunc)slot.sl_func; + break; + case Py_nb_floor_divide: + ht->as_number.nb_floor_divide = (binaryfunc)slot.sl_func; + break; + case Py_nb_index: + ht->as_number.nb_index = (unaryfunc)slot.sl_func; + break; + case Py_nb_inplace_add: + ht->as_number.nb_inplace_add = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_and: + ht->as_number.nb_inplace_and = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_floor_divide: + ht->as_number.nb_inplace_floor_divide = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_lshift: + ht->as_number.nb_inplace_lshift = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_multiply: + ht->as_number.nb_inplace_multiply = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_or: + ht->as_number.nb_inplace_or = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_power: + ht->as_number.nb_inplace_power = (ternaryfunc)slot.sl_func; + break; + case Py_nb_inplace_remainder: + ht->as_number.nb_inplace_remainder = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_rshift: + ht->as_number.nb_inplace_rshift = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_subtract: + ht->as_number.nb_inplace_subtract = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_true_divide: + ht->as_number.nb_inplace_true_divide = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_xor: + ht->as_number.nb_inplace_xor = (binaryfunc)slot.sl_func; + break; + case Py_nb_int: + ht->as_number.nb_int = (unaryfunc)slot.sl_func; + break; + case Py_nb_invert: + ht->as_number.nb_invert = (unaryfunc)slot.sl_func; + break; + case Py_nb_lshift: + ht->as_number.nb_lshift = (binaryfunc)slot.sl_func; + break; + case Py_nb_multiply: + ht->as_number.nb_multiply = (binaryfunc)slot.sl_func; + break; + case Py_nb_negative: + ht->as_number.nb_negative = (unaryfunc)slot.sl_func; + break; + case Py_nb_or: + ht->as_number.nb_or = (binaryfunc)slot.sl_func; + break; + case Py_nb_positive: + ht->as_number.nb_positive = (unaryfunc)slot.sl_func; + break; + case Py_nb_power: + ht->as_number.nb_power = (ternaryfunc)slot.sl_func; + break; + case Py_nb_remainder: + ht->as_number.nb_remainder = (binaryfunc)slot.sl_func; + break; + case Py_nb_rshift: + ht->as_number.nb_rshift = (binaryfunc)slot.sl_func; + break; + case Py_nb_subtract: + ht->as_number.nb_subtract = (binaryfunc)slot.sl_func; + break; + case Py_nb_true_divide: + ht->as_number.nb_true_divide = (binaryfunc)slot.sl_func; + break; + case Py_nb_xor: + ht->as_number.nb_xor = (binaryfunc)slot.sl_func; + break; + case Py_sq_ass_item: + ht->as_sequence.sq_ass_item = (ssizeobjargproc)slot.sl_func; + break; + case Py_sq_concat: + ht->as_sequence.sq_concat = (binaryfunc)slot.sl_func; + break; + case Py_sq_contains: + ht->as_sequence.sq_contains = (objobjproc)slot.sl_func; + break; + case Py_sq_inplace_concat: + ht->as_sequence.sq_inplace_concat = (binaryfunc)slot.sl_func; + break; + case Py_sq_inplace_repeat: + ht->as_sequence.sq_inplace_repeat = (ssizeargfunc)slot.sl_func; + break; + case Py_sq_item: + ht->as_sequence.sq_item = (ssizeargfunc)slot.sl_func; + break; + case Py_sq_length: + ht->as_sequence.sq_length = (lenfunc)slot.sl_func; + break; + case Py_sq_repeat: + ht->as_sequence.sq_repeat = (ssizeargfunc)slot.sl_func; + break; + case Py_tp_alloc: + ht->ht_type.tp_alloc = (allocfunc)slot.sl_func; + break; + case Py_tp_base: + ht->ht_type.tp_base = slot.sl_ptr; + break; + case Py_tp_bases: + ht->ht_type.tp_bases = slot.sl_ptr; + break; + case Py_tp_call: + ht->ht_type.tp_call = (ternaryfunc)slot.sl_func; + break; + case Py_tp_clear: + ht->ht_type.tp_clear = (inquiry)slot.sl_func; + break; + case Py_tp_dealloc: + ht->ht_type.tp_dealloc = (destructor)slot.sl_func; + break; + case Py_tp_del: + ht->ht_type.tp_del = (destructor)slot.sl_func; + break; + case Py_tp_descr_get: + ht->ht_type.tp_descr_get = (descrgetfunc)slot.sl_func; + break; + case Py_tp_descr_set: + ht->ht_type.tp_descr_set = (descrsetfunc)slot.sl_func; + break; + case Py_tp_doc: + ht->ht_type.tp_doc = slot.sl_ptr; + break; + case Py_tp_getattr: + ht->ht_type.tp_getattr = (getattrfunc)slot.sl_func; + break; + case Py_tp_getattro: + ht->ht_type.tp_getattro = (getattrofunc)slot.sl_func; + break; + case Py_tp_hash: + ht->ht_type.tp_hash = (hashfunc)slot.sl_func; + break; + case Py_tp_init: + ht->ht_type.tp_init = (initproc)slot.sl_func; + break; + case Py_tp_is_gc: + ht->ht_type.tp_is_gc = (inquiry)slot.sl_func; + break; + case Py_tp_iter: + ht->ht_type.tp_iter = (getiterfunc)slot.sl_func; + break; + case Py_tp_iternext: + ht->ht_type.tp_iternext = (iternextfunc)slot.sl_func; + break; + case Py_tp_methods: + ht->ht_type.tp_methods = slot.sl_ptr; + break; + case Py_tp_new: + ht->ht_type.tp_new = (newfunc)slot.sl_func; + break; + case Py_tp_repr: + ht->ht_type.tp_repr = (reprfunc)slot.sl_func; + break; + case Py_tp_richcompare: + ht->ht_type.tp_richcompare = (richcmpfunc)slot.sl_func; + break; + case Py_tp_setattr: + ht->ht_type.tp_setattr = (setattrfunc)slot.sl_func; + break; + case Py_tp_setattro: + ht->ht_type.tp_setattro = (setattrofunc)slot.sl_func; + break; + case Py_tp_str: + ht->ht_type.tp_str = (reprfunc)slot.sl_func; + break; + case Py_tp_traverse: + ht->ht_type.tp_traverse = (traverseproc)slot.sl_func; + break; + case Py_tp_members: + ht->ht_type.tp_members = slot.sl_ptr; + break; + case Py_tp_getset: + ht->ht_type.tp_getset = slot.sl_ptr; + break; + case Py_tp_free: + ht->ht_type.tp_free = (freefunc)slot.sl_func; + break; + case Py_nb_matrix_multiply: + ht->as_number.nb_matrix_multiply = (binaryfunc)slot.sl_func; + break; + case Py_nb_inplace_matrix_multiply: + ht->as_number.nb_inplace_matrix_multiply = (binaryfunc)slot.sl_func; + break; + case Py_am_await: + ht->as_async.am_await = (unaryfunc)slot.sl_func; + break; + case Py_am_aiter: + ht->as_async.am_aiter = (unaryfunc)slot.sl_func; + break; + case Py_am_anext: + ht->as_async.am_anext = (unaryfunc)slot.sl_func; + break; + case Py_tp_finalize: + ht->ht_type.tp_finalize = (destructor)slot.sl_func; + break; + case Py_am_send: + ht->as_async.am_send = (sendfunc)slot.sl_func; + break; + case Py_tp_vectorcall: + ht->ht_type.tp_vectorcall = (vectorcallfunc)slot.sl_func; + break; + case Py_bf_getbuffer: + ht->as_buffer.bf_getbuffer = (getbufferproc)slot.sl_func; + break; + case Py_bf_releasebuffer: + ht->as_buffer.bf_releasebuffer = (releasebufferproc)slot.sl_func; + break; + case Py_mp_ass_subscript: + ht->as_mapping.mp_ass_subscript = (objobjargproc)slot.sl_func; + break; + case Py_mp_length: + ht->as_mapping.mp_length = (lenfunc)slot.sl_func; + break; + } +} + +static inline _PySlot_DTYPE +_PySlot_get_dtype(uint16_t slot_id) +{ + switch (slot_id) { + case Py_slot_end: return _PySlot_DTYPE_VOID; + case Py_mp_subscript: return _PySlot_DTYPE_FUNC; + case Py_nb_absolute: return _PySlot_DTYPE_FUNC; + case Py_nb_add: return _PySlot_DTYPE_FUNC; + case Py_nb_and: return _PySlot_DTYPE_FUNC; + case Py_nb_bool: return _PySlot_DTYPE_FUNC; + case Py_nb_divmod: return _PySlot_DTYPE_FUNC; + case Py_nb_float: return _PySlot_DTYPE_FUNC; + case Py_nb_floor_divide: return _PySlot_DTYPE_FUNC; + case Py_nb_index: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_add: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_and: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_floor_divide: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_lshift: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_multiply: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_or: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_power: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_remainder: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_rshift: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_subtract: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_true_divide: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_xor: return _PySlot_DTYPE_FUNC; + case Py_nb_int: return _PySlot_DTYPE_FUNC; + case Py_nb_invert: return _PySlot_DTYPE_FUNC; + case Py_nb_lshift: return _PySlot_DTYPE_FUNC; + case Py_nb_multiply: return _PySlot_DTYPE_FUNC; + case Py_nb_negative: return _PySlot_DTYPE_FUNC; + case Py_nb_or: return _PySlot_DTYPE_FUNC; + case Py_nb_positive: return _PySlot_DTYPE_FUNC; + case Py_nb_power: return _PySlot_DTYPE_FUNC; + case Py_nb_remainder: return _PySlot_DTYPE_FUNC; + case Py_nb_rshift: return _PySlot_DTYPE_FUNC; + case Py_nb_subtract: return _PySlot_DTYPE_FUNC; + case Py_nb_true_divide: return _PySlot_DTYPE_FUNC; + case Py_nb_xor: return _PySlot_DTYPE_FUNC; + case Py_sq_ass_item: return _PySlot_DTYPE_FUNC; + case Py_sq_concat: return _PySlot_DTYPE_FUNC; + case Py_sq_contains: return _PySlot_DTYPE_FUNC; + case Py_sq_inplace_concat: return _PySlot_DTYPE_FUNC; + case Py_sq_inplace_repeat: return _PySlot_DTYPE_FUNC; + case Py_sq_item: return _PySlot_DTYPE_FUNC; + case Py_sq_length: return _PySlot_DTYPE_FUNC; + case Py_sq_repeat: return _PySlot_DTYPE_FUNC; + case Py_tp_alloc: return _PySlot_DTYPE_FUNC; + case Py_tp_base: return _PySlot_DTYPE_PTR; + case Py_tp_bases: return _PySlot_DTYPE_PTR; + case Py_tp_call: return _PySlot_DTYPE_FUNC; + case Py_tp_clear: return _PySlot_DTYPE_FUNC; + case Py_tp_dealloc: return _PySlot_DTYPE_FUNC; + case Py_tp_del: return _PySlot_DTYPE_FUNC; + case Py_tp_descr_get: return _PySlot_DTYPE_FUNC; + case Py_tp_descr_set: return _PySlot_DTYPE_FUNC; + case Py_tp_doc: return _PySlot_DTYPE_PTR; + case Py_tp_getattr: return _PySlot_DTYPE_FUNC; + case Py_tp_getattro: return _PySlot_DTYPE_FUNC; + case Py_tp_hash: return _PySlot_DTYPE_FUNC; + case Py_tp_init: return _PySlot_DTYPE_FUNC; + case Py_tp_is_gc: return _PySlot_DTYPE_FUNC; + case Py_tp_iter: return _PySlot_DTYPE_FUNC; + case Py_tp_iternext: return _PySlot_DTYPE_FUNC; + case Py_tp_methods: return _PySlot_DTYPE_PTR; + case Py_tp_new: return _PySlot_DTYPE_FUNC; + case Py_tp_repr: return _PySlot_DTYPE_FUNC; + case Py_tp_richcompare: return _PySlot_DTYPE_FUNC; + case Py_tp_setattr: return _PySlot_DTYPE_FUNC; + case Py_tp_setattro: return _PySlot_DTYPE_FUNC; + case Py_tp_str: return _PySlot_DTYPE_FUNC; + case Py_tp_traverse: return _PySlot_DTYPE_FUNC; + case Py_tp_members: return _PySlot_DTYPE_PTR; + case Py_tp_getset: return _PySlot_DTYPE_PTR; + case Py_tp_free: return _PySlot_DTYPE_FUNC; + case Py_nb_matrix_multiply: return _PySlot_DTYPE_FUNC; + case Py_nb_inplace_matrix_multiply: return _PySlot_DTYPE_FUNC; + case Py_am_await: return _PySlot_DTYPE_FUNC; + case Py_am_aiter: return _PySlot_DTYPE_FUNC; + case Py_am_anext: return _PySlot_DTYPE_FUNC; + case Py_tp_finalize: return _PySlot_DTYPE_FUNC; + case Py_am_send: return _PySlot_DTYPE_FUNC; + case Py_tp_vectorcall: return _PySlot_DTYPE_FUNC; + case Py_tp_token: return _PySlot_DTYPE_PTR; + case Py_mod_create: return _PySlot_DTYPE_FUNC; + case Py_mod_exec: return _PySlot_DTYPE_FUNC; + case Py_mod_multiple_interpreters: return _PySlot_DTYPE_UINT64; + case Py_mod_gil: return _PySlot_DTYPE_UINT64; + case Py_bf_getbuffer: return _PySlot_DTYPE_FUNC; + case Py_bf_releasebuffer: return _PySlot_DTYPE_FUNC; + case Py_mp_ass_subscript: return _PySlot_DTYPE_FUNC; + case Py_mp_length: return _PySlot_DTYPE_FUNC; + case Py_slot_subslots: return _PySlot_DTYPE_PTR; + case Py_tp_slots: return _PySlot_DTYPE_PTR; + case Py_mod_slots: return _PySlot_DTYPE_PTR; + case Py_tp_name: return _PySlot_DTYPE_PTR; + case Py_tp_basicsize: return _PySlot_DTYPE_SIZE; + case Py_tp_extra_basicsize: return _PySlot_DTYPE_SIZE; + case Py_tp_itemsize: return _PySlot_DTYPE_SIZE; + case Py_tp_flags: return _PySlot_DTYPE_UINT64; + case Py_mod_name: return _PySlot_DTYPE_PTR; + case Py_mod_doc: return _PySlot_DTYPE_PTR; + case Py_mod_state_size: return _PySlot_DTYPE_SIZE; + case Py_mod_methods: return _PySlot_DTYPE_PTR; + case Py_mod_state_traverse: return _PySlot_DTYPE_FUNC; + case Py_mod_state_clear: return _PySlot_DTYPE_FUNC; + case Py_mod_state_free: return _PySlot_DTYPE_FUNC; + case Py_tp_metaclass: return _PySlot_DTYPE_PTR; + case Py_tp_module: return _PySlot_DTYPE_PTR; + case Py_mod_abi: return _PySlot_DTYPE_PTR; + case Py_mod_token: return _PySlot_DTYPE_PTR; + default: return _PySlot_DTYPE_VOID; + } +} + +static inline _PySlot_PROBLEM_HANDLING +_PySlot_get_duplicate_handling(uint16_t slot_id) +{ + switch (slot_id) { + case Py_mp_subscript: + case Py_nb_absolute: + case Py_nb_add: + case Py_nb_and: + case Py_nb_bool: + case Py_nb_divmod: + case Py_nb_float: + case Py_nb_floor_divide: + case Py_nb_index: + case Py_nb_inplace_add: + case Py_nb_inplace_and: + case Py_nb_inplace_floor_divide: + case Py_nb_inplace_lshift: + case Py_nb_inplace_multiply: + case Py_nb_inplace_or: + case Py_nb_inplace_power: + case Py_nb_inplace_remainder: + case Py_nb_inplace_rshift: + case Py_nb_inplace_subtract: + case Py_nb_inplace_true_divide: + case Py_nb_inplace_xor: + case Py_nb_int: + case Py_nb_invert: + case Py_nb_lshift: + case Py_nb_multiply: + case Py_nb_negative: + case Py_nb_or: + case Py_nb_positive: + case Py_nb_power: + case Py_nb_remainder: + case Py_nb_rshift: + case Py_nb_subtract: + case Py_nb_true_divide: + case Py_nb_xor: + case Py_sq_ass_item: + case Py_sq_concat: + case Py_sq_contains: + case Py_sq_inplace_concat: + case Py_sq_inplace_repeat: + case Py_sq_item: + case Py_sq_length: + case Py_sq_repeat: + case Py_tp_alloc: + case Py_tp_base: + case Py_tp_bases: + case Py_tp_call: + case Py_tp_clear: + case Py_tp_dealloc: + case Py_tp_del: + case Py_tp_descr_get: + case Py_tp_descr_set: + case Py_tp_getattr: + case Py_tp_getattro: + case Py_tp_hash: + case Py_tp_init: + case Py_tp_is_gc: + case Py_tp_iter: + case Py_tp_iternext: + case Py_tp_methods: + case Py_tp_new: + case Py_tp_repr: + case Py_tp_richcompare: + case Py_tp_setattr: + case Py_tp_setattro: + case Py_tp_str: + case Py_tp_traverse: + case Py_tp_getset: + case Py_tp_free: + case Py_nb_matrix_multiply: + case Py_nb_inplace_matrix_multiply: + case Py_am_await: + case Py_am_aiter: + case Py_am_anext: + case Py_tp_finalize: + case Py_am_send: + case Py_tp_vectorcall: + case Py_tp_token: + case Py_bf_getbuffer: + case Py_bf_releasebuffer: + case Py_mp_ass_subscript: + case Py_mp_length: + return _PySlot_PROBLEM_DEPRECATED; + case Py_mod_exec: + case Py_mod_abi: + return _PySlot_PROBLEM_ALLOW; + default: + return _PySlot_PROBLEM_REJECT; + } +} + +static inline _PySlot_PROBLEM_HANDLING +_PySlot_get_null_handling(uint16_t slot_id) +{ + switch (slot_id) { + case Py_slot_end: + case Py_tp_doc: + case Py_tp_token: + case Py_mod_multiple_interpreters: + case Py_mod_gil: + case Py_slot_subslots: + case Py_tp_slots: + case Py_mod_slots: + case Py_tp_basicsize: + case Py_tp_extra_basicsize: + case Py_tp_itemsize: + case Py_tp_flags: + case Py_mod_state_size: + return _PySlot_PROBLEM_ALLOW; + case Py_mp_subscript: + case Py_nb_absolute: + case Py_nb_add: + case Py_nb_and: + case Py_nb_bool: + case Py_nb_divmod: + case Py_nb_float: + case Py_nb_floor_divide: + case Py_nb_index: + case Py_nb_inplace_add: + case Py_nb_inplace_and: + case Py_nb_inplace_floor_divide: + case Py_nb_inplace_lshift: + case Py_nb_inplace_multiply: + case Py_nb_inplace_or: + case Py_nb_inplace_power: + case Py_nb_inplace_remainder: + case Py_nb_inplace_rshift: + case Py_nb_inplace_subtract: + case Py_nb_inplace_true_divide: + case Py_nb_inplace_xor: + case Py_nb_int: + case Py_nb_invert: + case Py_nb_lshift: + case Py_nb_multiply: + case Py_nb_negative: + case Py_nb_or: + case Py_nb_positive: + case Py_nb_power: + case Py_nb_remainder: + case Py_nb_rshift: + case Py_nb_subtract: + case Py_nb_true_divide: + case Py_nb_xor: + case Py_sq_ass_item: + case Py_sq_concat: + case Py_sq_contains: + case Py_sq_inplace_concat: + case Py_sq_inplace_repeat: + case Py_sq_item: + case Py_sq_length: + case Py_sq_repeat: + case Py_tp_alloc: + case Py_tp_base: + case Py_tp_bases: + case Py_tp_call: + case Py_tp_clear: + case Py_tp_dealloc: + case Py_tp_del: + case Py_tp_descr_get: + case Py_tp_descr_set: + case Py_tp_getattr: + case Py_tp_getattro: + case Py_tp_hash: + case Py_tp_init: + case Py_tp_is_gc: + case Py_tp_iter: + case Py_tp_iternext: + case Py_tp_methods: + case Py_tp_new: + case Py_tp_repr: + case Py_tp_richcompare: + case Py_tp_setattr: + case Py_tp_setattro: + case Py_tp_str: + case Py_tp_traverse: + case Py_tp_getset: + case Py_tp_free: + case Py_nb_matrix_multiply: + case Py_nb_inplace_matrix_multiply: + case Py_am_await: + case Py_am_aiter: + case Py_am_anext: + case Py_tp_finalize: + case Py_am_send: + case Py_tp_vectorcall: + case Py_mod_create: + case Py_bf_getbuffer: + case Py_bf_releasebuffer: + case Py_mp_ass_subscript: + case Py_mp_length: + return _PySlot_PROBLEM_DEPRECATED; + default: + return _PySlot_PROBLEM_REJECT; + } +} + +static inline bool +_PySlot_get_must_be_static(uint16_t slot_id) +{ + switch (slot_id) { + case Py_tp_methods: return true; + case Py_tp_members: return true; + case Py_tp_getset: return true; + case Py_mod_methods: return true; + } + return false; +} + +#endif /* _PY_HAVE_INTERNAL_SLOTS_GENERATED_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stackref.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stackref.h index 42fdbeb21..ca4a7c216 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stackref.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stackref.h @@ -50,26 +50,64 @@ extern "C" { CPython refcounting operations on it! */ +#define Py_INT_TAG 3 +#define Py_TAG_INVALID 2 +#define Py_TAG_REFCNT 1 +#define Py_TAG_BITS 3 -#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) +#define Py_TAGGED_SHIFT 2 -#define Py_TAG_BITS 0 +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) PyAPI_FUNC(PyObject *) _Py_stackref_get_object(_PyStackRef ref); PyAPI_FUNC(PyObject *) _Py_stackref_close(_PyStackRef ref, const char *filename, int linenumber); -PyAPI_FUNC(_PyStackRef) _Py_stackref_create(PyObject *obj, const char *filename, int linenumber); +PyAPI_FUNC(_PyStackRef) _Py_stackref_create(PyObject *obj, uint16_t flags, const char *filename, int linenumber); PyAPI_FUNC(void) _Py_stackref_record_borrow(_PyStackRef ref, const char *filename, int linenumber); +PyAPI_FUNC(_PyStackRef) _Py_stackref_get_borrowed_from(_PyStackRef ref, const char *filename, int linenumber); +PyAPI_FUNC(void) _Py_stackref_set_borrowed_from(_PyStackRef ref, _PyStackRef borrowed_from, const char *filename, int linenumber); extern void _Py_stackref_associate(PyInterpreterState *interp, PyObject *obj, _PyStackRef ref); static const _PyStackRef PyStackRef_NULL = { .index = 0 }; +static const _PyStackRef PyStackRef_ERROR = { .index = (1 << Py_TAGGED_SHIFT) }; -// Use the first 3 even numbers for None, True and False. -// Odd numbers are reserved for (tagged) integers -#define PyStackRef_None ((_PyStackRef){ .index = 2 } ) -#define PyStackRef_False ((_PyStackRef){ .index = 4 }) -#define PyStackRef_True ((_PyStackRef){ .index = 6 }) +#define PyStackRef_None ((_PyStackRef){ .index = (2 << Py_TAGGED_SHIFT) } ) +#define _Py_STACKREF_FALSE_INDEX (3 << Py_TAGGED_SHIFT) +#define _Py_STACKREF_TRUE_INDEX (4 << Py_TAGGED_SHIFT) +#define PyStackRef_False ((_PyStackRef){ .index = _Py_STACKREF_FALSE_INDEX }) +#define PyStackRef_True ((_PyStackRef){ .index = _Py_STACKREF_TRUE_INDEX }) -#define INITIAL_STACKREF_INDEX 8 +#define INITIAL_STACKREF_INDEX (5 << Py_TAGGED_SHIFT) + +#define PyStackRef_ZERO_BITS PyStackRef_NULL + +static inline _PyStackRef +PyStackRef_Wrap(void *ptr) +{ + assert(ptr != NULL); +#ifdef Py_DEBUG + assert(((uint64_t)ptr & Py_TAG_BITS) == 0); + return (_PyStackRef){ .index = ((uint64_t)ptr) | Py_TAG_INVALID }; +#else + return (_PyStackRef){ .index = (uint64_t)ptr }; +#endif +} + +static inline void * +PyStackRef_Unwrap(_PyStackRef ref) +{ +#ifdef Py_DEBUG + assert ((ref.index & Py_TAG_BITS) == Py_TAG_INVALID); + return (void *)(ref.index & ~Py_TAG_BITS); +#else + return (void *)(ref.index); +#endif +} + +static inline int +PyStackRef_RefcountOnObject(_PyStackRef ref) +{ + return (ref.index & Py_TAG_REFCNT) == 0; +} static inline int PyStackRef_IsNull(_PyStackRef ref) @@ -77,6 +115,25 @@ PyStackRef_IsNull(_PyStackRef ref) return ref.index == 0; } +static inline bool +PyStackRef_IsError(_PyStackRef ref) +{ + return ref.index == (1 << Py_TAGGED_SHIFT); +} + +static inline bool +PyStackRef_IsMalformed(_PyStackRef ref) +{ + return (ref.index & Py_TAG_BITS) == Py_TAG_INVALID; +} + +static inline bool +PyStackRef_IsValid(_PyStackRef ref) +{ + /* Invalid values are ERROR and NULL */ + return !PyStackRef_IsError(ref) && !PyStackRef_IsNull(ref); +} + static inline int PyStackRef_IsTrue(_PyStackRef ref) { @@ -95,74 +152,86 @@ PyStackRef_IsNone(_PyStackRef ref) return _Py_stackref_get_object(ref) == Py_None; } +static inline bool +PyStackRef_IsTaggedInt(_PyStackRef ref) +{ + return (ref.index & Py_TAG_BITS) == Py_INT_TAG; +} + static inline PyObject * _PyStackRef_AsPyObjectBorrow(_PyStackRef ref, const char *filename, int linenumber) { - assert((ref.index & 1) == 0); + assert(!PyStackRef_IsError(ref)); + assert(!PyStackRef_IsTaggedInt(ref)); _Py_stackref_record_borrow(ref, filename, linenumber); return _Py_stackref_get_object(ref); } - #define PyStackRef_AsPyObjectBorrow(REF) _PyStackRef_AsPyObjectBorrow((REF), __FILE__, __LINE__) static inline PyObject * _PyStackRef_AsPyObjectSteal(_PyStackRef ref, const char *filename, int linenumber) { - return _Py_stackref_close(ref, filename, linenumber); + PyObject *obj = _Py_stackref_close(ref, filename, linenumber); + if (PyStackRef_RefcountOnObject(ref)) { + return obj; + } + return Py_NewRef(obj); } #define PyStackRef_AsPyObjectSteal(REF) _PyStackRef_AsPyObjectSteal((REF), __FILE__, __LINE__) static inline _PyStackRef _PyStackRef_FromPyObjectNew(PyObject *obj, const char *filename, int linenumber) { - Py_INCREF(obj); - return _Py_stackref_create(obj, filename, linenumber); + assert(obj != NULL); + uint16_t flags = 0; + if (!_Py_IsImmortal(obj)) { + _Py_INCREF_MORTAL(obj); + } else { + flags = Py_TAG_REFCNT; + } + return _Py_stackref_create(obj, flags, filename, linenumber); } #define PyStackRef_FromPyObjectNew(obj) _PyStackRef_FromPyObjectNew(_PyObject_CAST(obj), __FILE__, __LINE__) static inline _PyStackRef _PyStackRef_FromPyObjectSteal(PyObject *obj, const char *filename, int linenumber) { - return _Py_stackref_create(obj, filename, linenumber); + assert(obj != NULL); + uint16_t flags = 0; + if (_Py_IsImmortal(obj)) { + flags = Py_TAG_REFCNT; + } + return _Py_stackref_create(obj, flags, filename, linenumber); } #define PyStackRef_FromPyObjectSteal(obj) _PyStackRef_FromPyObjectSteal(_PyObject_CAST(obj), __FILE__, __LINE__) static inline _PyStackRef _PyStackRef_FromPyObjectBorrow(PyObject *obj, const char *filename, int linenumber) { - return _Py_stackref_create(obj, filename, linenumber); + return _Py_stackref_create(obj, Py_TAG_REFCNT, filename, linenumber); } #define PyStackRef_FromPyObjectBorrow(obj) _PyStackRef_FromPyObjectBorrow(_PyObject_CAST(obj), __FILE__, __LINE__) -static inline _PyStackRef -_PyStackRef_FromPyObjectImmortal(PyObject *obj, const char *filename, int linenumber) -{ - assert(_Py_IsImmortal(obj)); - return _Py_stackref_create(obj, filename, linenumber); -} -#define PyStackRef_FromPyObjectImmortal(obj) _PyStackRef_FromPyObjectImmortal(_PyObject_CAST(obj), __FILE__, __LINE__) - -static inline bool -PyStackRef_IsTaggedInt(_PyStackRef ref) -{ - return (ref.index & 1) == 1; -} - static inline void _PyStackRef_CLOSE(_PyStackRef ref, const char *filename, int linenumber) { + assert(!PyStackRef_IsError(ref)); + assert(!PyStackRef_IsNull(ref)); if (PyStackRef_IsTaggedInt(ref)) { return; } PyObject *obj = _Py_stackref_close(ref, filename, linenumber); - Py_DECREF(obj); + assert(Py_REFCNT(obj) > 0); + if (PyStackRef_RefcountOnObject(ref)) { + Py_DECREF(obj); + } } #define PyStackRef_CLOSE(REF) _PyStackRef_CLOSE((REF), __FILE__, __LINE__) - static inline void _PyStackRef_XCLOSE(_PyStackRef ref, const char *filename, int linenumber) { + assert(!PyStackRef_IsError(ref)); if (PyStackRef_IsNull(ref)) { return; } @@ -173,31 +242,57 @@ _PyStackRef_XCLOSE(_PyStackRef ref, const char *filename, int linenumber) static inline _PyStackRef _PyStackRef_DUP(_PyStackRef ref, const char *filename, int linenumber) { + assert(!PyStackRef_IsError(ref)); + assert(!PyStackRef_IsNull(ref)); if (PyStackRef_IsTaggedInt(ref)) { return ref; } - else { - PyObject *obj = _Py_stackref_get_object(ref); + PyObject *obj = _Py_stackref_get_object(ref); + uint16_t flags = 0; + if (PyStackRef_RefcountOnObject(ref)) { Py_INCREF(obj); - return _Py_stackref_create(obj, filename, linenumber); + } else { + flags = Py_TAG_REFCNT; } + _PyStackRef new_ref = _Py_stackref_create(obj, flags, filename, linenumber); + if (flags == Py_TAG_REFCNT && !_Py_IsImmortal(obj)) { + _PyStackRef borrowed_from = _Py_stackref_get_borrowed_from(ref, filename, linenumber); + _Py_stackref_set_borrowed_from(new_ref, borrowed_from, filename, linenumber); + } + return new_ref; } #define PyStackRef_DUP(REF) _PyStackRef_DUP(REF, __FILE__, __LINE__) -extern void _PyStackRef_CLOSE_SPECIALIZED(_PyStackRef ref, destructor destruct, const char *filename, int linenumber); -#define PyStackRef_CLOSE_SPECIALIZED(REF, DESTRUCT) _PyStackRef_CLOSE_SPECIALIZED(REF, DESTRUCT, __FILE__, __LINE__) - -static inline _PyStackRef -PyStackRef_MakeHeapSafe(_PyStackRef ref) +static inline void +_PyStackRef_CLOSE_SPECIALIZED(_PyStackRef ref, destructor destruct, const char *filename, int linenumber) { - return ref; + assert(!PyStackRef_IsError(ref)); + assert(!PyStackRef_IsNull(ref)); + assert(!PyStackRef_IsTaggedInt(ref)); + PyObject *obj = _Py_stackref_close(ref, filename, linenumber); + assert(Py_REFCNT(obj) > 0); + if (PyStackRef_RefcountOnObject(ref)) { + _Py_DECREF_SPECIALIZED(obj, destruct); + } } +#define PyStackRef_CLOSE_SPECIALIZED(REF, DESTRUCT) _PyStackRef_CLOSE_SPECIALIZED(REF, DESTRUCT, __FILE__, __LINE__) static inline _PyStackRef -PyStackRef_Borrow(_PyStackRef ref) +_PyStackRef_Borrow(_PyStackRef ref, const char *filename, int linenumber) { - return PyStackRef_DUP(ref); + assert(!PyStackRef_IsError(ref)); + assert(!PyStackRef_IsNull(ref)); + if (PyStackRef_IsTaggedInt(ref)) { + return ref; + } + PyObject *obj = _Py_stackref_get_object(ref); + _PyStackRef new_ref = _Py_stackref_create(obj, Py_TAG_REFCNT, filename, linenumber); + if (!_Py_IsImmortal(obj)) { + _Py_stackref_set_borrowed_from(new_ref, ref, filename, linenumber); + } + return new_ref; } +#define PyStackRef_Borrow(REF) _PyStackRef_Borrow((REF), __FILE__, __LINE__) #define PyStackRef_CLEAR(REF) \ do { \ @@ -210,28 +305,56 @@ PyStackRef_Borrow(_PyStackRef ref) static inline _PyStackRef _PyStackRef_FromPyObjectStealMortal(PyObject *obj, const char *filename, int linenumber) { + assert(obj != NULL); assert(!_Py_IsImmortal(obj)); - return _Py_stackref_create(obj, filename, linenumber); + return _Py_stackref_create(obj, 0, filename, linenumber); } #define PyStackRef_FromPyObjectStealMortal(obj) _PyStackRef_FromPyObjectStealMortal(_PyObject_CAST(obj), __FILE__, __LINE__) static inline bool PyStackRef_IsHeapSafe(_PyStackRef ref) { - return true; + if ((ref.index & Py_TAG_BITS) != Py_TAG_REFCNT || PyStackRef_IsNull(ref)) { + // Tagged ints and ERROR are included. + return true; + } + + PyObject *obj = _Py_stackref_get_object(ref); + return _Py_IsImmortal(obj); } +static inline _PyStackRef +_PyStackRef_MakeHeapSafe(_PyStackRef ref, const char *filename, int linenumber) +{ + // Special references that can't be closed. + if (ref.index < INITIAL_STACKREF_INDEX) { + return ref; + } + + bool heap_safe = PyStackRef_IsHeapSafe(ref); + PyObject *obj = _Py_stackref_close(ref, filename, linenumber); + uint16_t flags = 0; + if (heap_safe) { + // Close old ref and create a new one with the same flags. + // This is necessary for correct borrow checking. + flags = ref.index & Py_TAG_BITS; + } else { + Py_INCREF(obj); + } + return _Py_stackref_create(obj, flags, filename, linenumber); +} +#define PyStackRef_MakeHeapSafe(REF) _PyStackRef_MakeHeapSafe(REF, __FILE__, __LINE__) + static inline _PyStackRef _PyStackRef_FromPyObjectNewMortal(PyObject *obj, const char *filename, int linenumber) { + assert(obj != NULL); assert(!_Py_IsStaticImmortal(obj)); Py_INCREF(obj); - return _Py_stackref_create(obj, filename, linenumber); + return _Py_stackref_create(obj, 0, filename, linenumber); } #define PyStackRef_FromPyObjectNewMortal(obj) _PyStackRef_FromPyObjectNewMortal(_PyObject_CAST(obj), __FILE__, __LINE__) -#define PyStackRef_RefcountOnObject(REF) 1 - extern int PyStackRef_Is(_PyStackRef a, _PyStackRef b); extern bool PyStackRef_IsTaggedInt(_PyStackRef ref); @@ -240,225 +363,100 @@ extern intptr_t PyStackRef_UntagInt(_PyStackRef ref); extern _PyStackRef PyStackRef_TagInt(intptr_t i); +/* Increments a tagged int, but does not check for overflow */ +extern _PyStackRef PyStackRef_IncrementTaggedIntNoOverflow(_PyStackRef ref); + extern bool PyStackRef_IsNullOrInt(_PyStackRef ref); #else -#define Py_INT_TAG 3 -#define Py_TAG_REFCNT 1 +static const _PyStackRef PyStackRef_ERROR = { .bits = Py_TAG_INVALID }; -static inline bool -PyStackRef_IsTaggedInt(_PyStackRef i) -{ - return (i.bits & Py_INT_TAG) == Py_INT_TAG; -} +/* For use in the JIT to clear an unused value. + * PyStackRef_ZERO_BITS has no meaning and should not be used other than by the JIT. */ +static const _PyStackRef PyStackRef_ZERO_BITS = { .bits = 0 }; +/* Wrap a pointer in a stack ref. + * The resulting stack reference is not safe and should only be used + * in the interpreter to pass values from one uop to another. + * The GC should never see one of these stack refs. */ static inline _PyStackRef -PyStackRef_TagInt(intptr_t i) -{ - assert(Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, (i << 2), 2) == i); - return (_PyStackRef){ .bits = ((((uintptr_t)i) << 2) | Py_INT_TAG) }; -} - -static inline intptr_t -PyStackRef_UntagInt(_PyStackRef i) -{ - assert((i.bits & Py_INT_TAG) == Py_INT_TAG); - intptr_t val = (intptr_t)i.bits; - return Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, val, 2); -} - - -#ifdef Py_GIL_DISABLED - -#define Py_TAG_DEFERRED Py_TAG_REFCNT - -#define Py_TAG_PTR ((uintptr_t)0) -#define Py_TAG_BITS ((uintptr_t)1) - - -static const _PyStackRef PyStackRef_NULL = { .bits = Py_TAG_DEFERRED}; -#define PyStackRef_IsNull(stackref) ((stackref).bits == PyStackRef_NULL.bits) -#define PyStackRef_True ((_PyStackRef){.bits = ((uintptr_t)&_Py_TrueStruct) | Py_TAG_DEFERRED }) -#define PyStackRef_False ((_PyStackRef){.bits = ((uintptr_t)&_Py_FalseStruct) | Py_TAG_DEFERRED }) -#define PyStackRef_None ((_PyStackRef){.bits = ((uintptr_t)&_Py_NoneStruct) | Py_TAG_DEFERRED }) - -// Checks that mask out the deferred bit in the free threading build. -#define PyStackRef_IsNone(ref) (PyStackRef_AsPyObjectBorrow(ref) == Py_None) -#define PyStackRef_IsTrue(ref) (PyStackRef_AsPyObjectBorrow(ref) == Py_True) -#define PyStackRef_IsFalse(ref) (PyStackRef_AsPyObjectBorrow(ref) == Py_False) - -#define PyStackRef_IsNullOrInt(stackref) (PyStackRef_IsNull(stackref) || PyStackRef_IsTaggedInt(stackref)) - -static inline PyObject * -PyStackRef_AsPyObjectBorrow(_PyStackRef stackref) -{ - PyObject *cleared = ((PyObject *)((stackref).bits & (~Py_TAG_BITS))); - return cleared; -} - -#define PyStackRef_IsDeferred(ref) (((ref).bits & Py_TAG_BITS) == Py_TAG_DEFERRED) - -static inline PyObject * -PyStackRef_NotDeferred_AsPyObject(_PyStackRef stackref) -{ - assert(!PyStackRef_IsDeferred(stackref)); - return (PyObject *)stackref.bits; -} - -static inline PyObject * -PyStackRef_AsPyObjectSteal(_PyStackRef stackref) +PyStackRef_Wrap(void *ptr) { - assert(!PyStackRef_IsNull(stackref)); - if (PyStackRef_IsDeferred(stackref)) { - return Py_NewRef(PyStackRef_AsPyObjectBorrow(stackref)); - } - return PyStackRef_AsPyObjectBorrow(stackref); -} - -static inline _PyStackRef -_PyStackRef_FromPyObjectSteal(PyObject *obj) -{ - assert(obj != NULL); - // Make sure we don't take an already tagged value. - assert(((uintptr_t)obj & Py_TAG_BITS) == 0); - return (_PyStackRef){ .bits = (uintptr_t)obj }; + assert(ptr != NULL); +#ifdef Py_DEBUG + assert(((uintptr_t)ptr & Py_TAG_BITS) == 0); + return (_PyStackRef){ .bits = ((uintptr_t)ptr) | Py_TAG_INVALID }; +#else + return (_PyStackRef){ .bits = (uintptr_t)ptr }; +#endif } -# define PyStackRef_FromPyObjectSteal(obj) _PyStackRef_FromPyObjectSteal(_PyObject_CAST(obj)) -static inline _PyStackRef -PyStackRef_FromPyObjectBorrow(PyObject *obj) +static inline void * +PyStackRef_Unwrap(_PyStackRef ref) { - assert(obj != NULL); - assert(((uintptr_t)obj & Py_TAG_BITS) == 0); - return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_DEFERRED }; +#ifdef Py_DEBUG + assert ((ref.bits & Py_TAG_BITS) == Py_TAG_INVALID); + return (void *)(ref.bits & ~Py_TAG_BITS); +#else + return (void *)(ref.bits); +#endif } static inline bool -PyStackRef_IsHeapSafe(_PyStackRef stackref) +PyStackRef_IsError(_PyStackRef ref) { - if (PyStackRef_IsDeferred(stackref) && !PyStackRef_IsTaggedInt(stackref)) { - PyObject *obj = PyStackRef_AsPyObjectBorrow(stackref); - return obj == NULL || _Py_IsImmortal(obj) || _PyObject_HasDeferredRefcount(obj); - } - return true; + return ref.bits == Py_TAG_INVALID; } -static inline _PyStackRef -PyStackRef_MakeHeapSafe(_PyStackRef stackref) +static inline bool +PyStackRef_IsMalformed(_PyStackRef ref) { - if (PyStackRef_IsHeapSafe(stackref)) { - return stackref; - } - PyObject *obj = PyStackRef_AsPyObjectBorrow(stackref); - return (_PyStackRef){ .bits = (uintptr_t)(Py_NewRef(obj)) | Py_TAG_PTR }; + return (ref.bits & Py_TAG_BITS) == Py_TAG_INVALID; } -static inline _PyStackRef -PyStackRef_FromPyObjectStealMortal(PyObject *obj) +static inline bool +PyStackRef_IsValid(_PyStackRef ref) { - assert(obj != NULL); - assert(!_Py_IsImmortal(obj)); - // Make sure we don't take an already tagged value. - assert(((uintptr_t)obj & Py_TAG_BITS) == 0); - return (_PyStackRef){ .bits = (uintptr_t)obj }; + /* Invalid values are ERROR and NULL */ + return ref.bits >= Py_INT_TAG; } -static inline _PyStackRef -PyStackRef_FromPyObjectNew(PyObject *obj) +static inline bool +PyStackRef_IsTaggedInt(_PyStackRef i) { - // Make sure we don't take an already tagged value. - assert(((uintptr_t)obj & Py_TAG_BITS) == 0); - assert(obj != NULL); - if (_PyObject_HasDeferredRefcount(obj)) { - return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_DEFERRED }; - } - else { - return (_PyStackRef){ .bits = (uintptr_t)(Py_NewRef(obj)) | Py_TAG_PTR }; - } + return (i.bits & Py_TAG_BITS) == Py_INT_TAG; } -#define PyStackRef_FromPyObjectNew(obj) PyStackRef_FromPyObjectNew(_PyObject_CAST(obj)) static inline _PyStackRef -PyStackRef_FromPyObjectImmortal(PyObject *obj) -{ - // Make sure we don't take an already tagged value. - assert(((uintptr_t)obj & Py_TAG_BITS) == 0); - assert(obj != NULL); - assert(_Py_IsImmortal(obj)); - return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_DEFERRED }; -} -#define PyStackRef_FromPyObjectImmortal(obj) PyStackRef_FromPyObjectImmortal(_PyObject_CAST(obj)) - -#define PyStackRef_CLOSE(REF) \ - do { \ - _PyStackRef _close_tmp = (REF); \ - assert(!PyStackRef_IsNull(_close_tmp)); \ - if (!PyStackRef_IsDeferred(_close_tmp)) { \ - Py_DECREF(PyStackRef_AsPyObjectBorrow(_close_tmp)); \ - } \ - } while (0) - -static inline void -PyStackRef_CLOSE_SPECIALIZED(_PyStackRef ref, destructor destruct) +PyStackRef_TagInt(intptr_t i) { - (void)destruct; - PyStackRef_CLOSE(ref); + assert(Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, (intptr_t)(((uintptr_t)i) << Py_TAGGED_SHIFT), + Py_TAGGED_SHIFT) == i); + return (_PyStackRef){ .bits = ((((uintptr_t)i) << Py_TAGGED_SHIFT) | Py_INT_TAG) }; } -static inline _PyStackRef -PyStackRef_DUP(_PyStackRef stackref) +static inline intptr_t +PyStackRef_UntagInt(_PyStackRef i) { - assert(!PyStackRef_IsNull(stackref)); - if (PyStackRef_IsDeferred(stackref)) { - return stackref; - } - Py_INCREF(PyStackRef_AsPyObjectBorrow(stackref)); - return stackref; + assert(PyStackRef_IsTaggedInt(i)); + intptr_t val = (intptr_t)i.bits; + return Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, val, Py_TAGGED_SHIFT); } -static inline _PyStackRef -PyStackRef_Borrow(_PyStackRef stackref) -{ - return (_PyStackRef){ .bits = stackref.bits | Py_TAG_DEFERRED }; -} -// Convert a possibly deferred reference to a strong reference. static inline _PyStackRef -PyStackRef_AsStrongReference(_PyStackRef stackref) +PyStackRef_IncrementTaggedIntNoOverflow(_PyStackRef ref) { - return PyStackRef_FromPyObjectSteal(PyStackRef_AsPyObjectSteal(stackref)); + assert((ref.bits & Py_TAG_BITS) == Py_INT_TAG); // Is tagged int + assert((ref.bits & (~Py_TAG_BITS)) != (INTPTR_MAX & (~Py_TAG_BITS))); // Isn't about to overflow + return (_PyStackRef){ .bits = ref.bits + (1 << Py_TAGGED_SHIFT) }; } -#define PyStackRef_XCLOSE(stackref) \ - do { \ - _PyStackRef _tmp = (stackref); \ - if (!PyStackRef_IsNull(_tmp)) { \ - PyStackRef_CLOSE(_tmp); \ - } \ - } while (0); - -#define PyStackRef_CLEAR(op) \ - do { \ - _PyStackRef *_tmp_op_ptr = &(op); \ - _PyStackRef _tmp_old_op = (*_tmp_op_ptr); \ - if (!PyStackRef_IsNull(_tmp_old_op)) { \ - *_tmp_op_ptr = PyStackRef_NULL; \ - PyStackRef_CLOSE(_tmp_old_op); \ - } \ - } while (0) - -#define PyStackRef_FromPyObjectNewMortal PyStackRef_FromPyObjectNew - -#else // Py_GIL_DISABLED - -// With GIL - /* References to immortal objects always have their tag bit set to Py_TAG_REFCNT * as they can (must) have their reclamation deferred */ -#define Py_TAG_BITS 3 #if _Py_IMMORTAL_FLAGS != Py_TAG_REFCNT # error "_Py_IMMORTAL_FLAGS != Py_TAG_REFCNT" #endif @@ -474,13 +472,24 @@ static const _PyStackRef PyStackRef_NULL = { .bits = PyStackRef_NULL_BITS }; #define PyStackRef_False ((_PyStackRef){.bits = ((uintptr_t)&_Py_FalseStruct) | Py_TAG_REFCNT }) #define PyStackRef_None ((_PyStackRef){.bits = ((uintptr_t)&_Py_NoneStruct) | Py_TAG_REFCNT }) +#ifdef Py_GIL_DISABLED +// Checks that mask out the deferred bit in the free threading build. +#define PyStackRef_IsNone(REF) (((REF).bits & ~Py_TAG_REFCNT) == (uintptr_t)&_Py_NoneStruct) +#define PyStackRef_IsTrue(REF) (((REF).bits & ~Py_TAG_REFCNT) == (uintptr_t)&_Py_TrueStruct) +#define PyStackRef_IsFalse(REF) (((REF).bits & ~Py_TAG_REFCNT) == (uintptr_t)&_Py_FalseStruct) +#else #define PyStackRef_IsTrue(REF) ((REF).bits == (((uintptr_t)&_Py_TrueStruct) | Py_TAG_REFCNT)) #define PyStackRef_IsFalse(REF) ((REF).bits == (((uintptr_t)&_Py_FalseStruct) | Py_TAG_REFCNT)) #define PyStackRef_IsNone(REF) ((REF).bits == (((uintptr_t)&_Py_NoneStruct) | Py_TAG_REFCNT)) +#endif -#ifdef Py_DEBUG +#define PyStackRef_IsNullOrInt(stackref) (PyStackRef_IsNull(stackref) || PyStackRef_IsTaggedInt(stackref)) -static inline void PyStackRef_CheckValid(_PyStackRef ref) { +#if defined(Py_DEBUG) && !defined(Py_GIL_DISABLED) + +static inline void +PyStackRef_CheckValid(_PyStackRef ref) +{ assert(ref.bits != 0); int tag = ref.bits & Py_TAG_BITS; PyObject *obj = BITS_TO_PTR_MASKED(ref); @@ -531,6 +540,8 @@ PyStackRef_Borrow(_PyStackRef ref) static inline PyObject * PyStackRef_AsPyObjectSteal(_PyStackRef ref) { + assert(!PyStackRef_IsNull(ref)); + assert(!PyStackRef_IsTaggedInt(ref)); if (PyStackRef_RefcountOnObject(ref)) { return BITS_TO_PTR(ref); } @@ -543,21 +554,18 @@ static inline _PyStackRef PyStackRef_FromPyObjectSteal(PyObject *obj) { assert(obj != NULL); -#if SIZEOF_VOID_P > 4 - unsigned int tag = obj->ob_flags & Py_TAG_REFCNT; +#ifdef Py_GIL_DISABLED + return (_PyStackRef){ .bits = (uintptr_t)obj }; #else +# if SIZEOF_VOID_P > 4 + unsigned int tag = obj->ob_flags & Py_TAG_REFCNT; +# else unsigned int tag = _Py_IsImmortal(obj) ? Py_TAG_REFCNT : 0; -#endif +# endif _PyStackRef ref = ((_PyStackRef){.bits = ((uintptr_t)(obj)) | tag}); PyStackRef_CheckValid(ref); return ref; -} - -static inline _PyStackRef -PyStackRef_FromPyObjectBorrow(PyObject *obj) -{ - assert(obj != NULL); - return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_REFCNT }; +#endif } static inline _PyStackRef @@ -565,7 +573,7 @@ PyStackRef_FromPyObjectStealMortal(PyObject *obj) { assert(obj != NULL); assert(!_Py_IsImmortal(obj)); - _PyStackRef ref = ((_PyStackRef){.bits = ((uintptr_t)(obj)) }); + _PyStackRef ref = (_PyStackRef){ .bits = (uintptr_t)obj }; PyStackRef_CheckValid(ref); return ref; } @@ -574,9 +582,15 @@ static inline _PyStackRef _PyStackRef_FromPyObjectNew(PyObject *obj) { assert(obj != NULL); +#ifdef Py_GIL_DISABLED + if (_PyObject_HasDeferredRefcount(obj)) { + return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_REFCNT }; + } +#else if (_Py_IsImmortal(obj)) { - return (_PyStackRef){ .bits = ((uintptr_t)obj) | Py_TAG_REFCNT}; + return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_REFCNT }; } +#endif _Py_INCREF_MORTAL(obj); _PyStackRef ref = (_PyStackRef){ .bits = (uintptr_t)obj }; PyStackRef_CheckValid(ref); @@ -597,9 +611,9 @@ _PyStackRef_FromPyObjectNewMortal(PyObject *obj) /* Create a new reference from an object with an embedded reference count */ static inline _PyStackRef -PyStackRef_FromPyObjectImmortal(PyObject *obj) +PyStackRef_FromPyObjectBorrow(PyObject *obj) { - assert(_Py_IsImmortal(obj)); + assert(obj != NULL); return (_PyStackRef){ .bits = (uintptr_t)obj | Py_TAG_REFCNT}; } @@ -622,7 +636,15 @@ PyStackRef_DUP(_PyStackRef ref) static inline bool PyStackRef_IsHeapSafe(_PyStackRef ref) { - return (ref.bits & Py_TAG_BITS) != Py_TAG_REFCNT || ref.bits == PyStackRef_NULL_BITS || _Py_IsImmortal(BITS_TO_PTR_MASKED(ref)); +#ifdef Py_GIL_DISABLED + if ((ref.bits & Py_TAG_BITS) != Py_TAG_REFCNT) { + return true; + } + PyObject *obj = BITS_TO_PTR_MASKED(ref); + return obj == NULL || _PyObject_HasDeferredRefcount(obj); +#else + return (ref.bits & Py_TAG_BITS) != Py_TAG_REFCNT || ref.bits == PyStackRef_NULL_BITS || _Py_IsImmortal(BITS_TO_PTR_MASKED(ref)); +#endif } static inline _PyStackRef @@ -638,6 +660,13 @@ PyStackRef_MakeHeapSafe(_PyStackRef ref) return ref; } +// Convert a possibly deferred reference to a strong reference. +static inline _PyStackRef +PyStackRef_AsStrongReference(_PyStackRef stackref) +{ + return PyStackRef_FromPyObjectSteal(PyStackRef_AsPyObjectSteal(stackref)); +} + #ifdef _WIN32 #define PyStackRef_CLOSE(REF) \ do { \ @@ -655,12 +684,6 @@ PyStackRef_CLOSE(_PyStackRef ref) } #endif -static inline bool -PyStackRef_IsNullOrInt(_PyStackRef ref) -{ - return PyStackRef_IsNull(ref) || PyStackRef_IsTaggedInt(ref); -} - static inline void PyStackRef_CLOSE_SPECIALIZED(_PyStackRef ref, destructor destruct) { @@ -693,8 +716,6 @@ PyStackRef_XCLOSE(_PyStackRef ref) } while (0) -#endif // Py_GIL_DISABLED - // Note: this is a macro because MSVC (Windows) has trouble inlining it. #define PyStackRef_Is(a, b) (((a).bits & (~Py_TAG_REFCNT)) == ((b).bits & (~Py_TAG_REFCNT))) @@ -702,7 +723,13 @@ PyStackRef_XCLOSE(_PyStackRef ref) #endif // !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) -#define PyStackRef_TYPE(stackref) Py_TYPE(PyStackRef_AsPyObjectBorrow(stackref)) +static inline PyTypeObject * +PyStackRef_TYPE(_PyStackRef stackref) { + if (PyStackRef_IsTaggedInt(stackref)) { + return &PyLong_Type; + } + return Py_TYPE(PyStackRef_AsPyObjectBorrow(stackref)); +} // Converts a PyStackRef back to a PyObject *, converting the // stackref to a new reference. @@ -710,42 +737,30 @@ PyStackRef_XCLOSE(_PyStackRef ref) // StackRef type checks -static inline bool -PyStackRef_GenCheck(_PyStackRef stackref) -{ - return PyGen_Check(PyStackRef_AsPyObjectBorrow(stackref)); -} +#define STACKREF_CHECK_FUNC(T) \ + static inline bool \ + PyStackRef_ ## T ## Check(_PyStackRef stackref) { \ + if (PyStackRef_IsTaggedInt(stackref)) { \ + return false; \ + } \ + return Py ## T ## _Check(PyStackRef_AsPyObjectBorrow(stackref)); \ + } -static inline bool -PyStackRef_BoolCheck(_PyStackRef stackref) -{ - return PyBool_Check(PyStackRef_AsPyObjectBorrow(stackref)); -} +STACKREF_CHECK_FUNC(Gen) +STACKREF_CHECK_FUNC(Bool) +STACKREF_CHECK_FUNC(ExceptionInstance) +STACKREF_CHECK_FUNC(Code) +STACKREF_CHECK_FUNC(Function) static inline bool PyStackRef_LongCheck(_PyStackRef stackref) { + if (PyStackRef_IsTaggedInt(stackref)) { + return true; + } return PyLong_Check(PyStackRef_AsPyObjectBorrow(stackref)); } -static inline bool -PyStackRef_ExceptionInstanceCheck(_PyStackRef stackref) -{ - return PyExceptionInstance_Check(PyStackRef_AsPyObjectBorrow(stackref)); -} - -static inline bool -PyStackRef_CodeCheck(_PyStackRef stackref) -{ - return PyCode_Check(PyStackRef_AsPyObjectBorrow(stackref)); -} - -static inline bool -PyStackRef_FunctionCheck(_PyStackRef stackref) -{ - return PyFunction_Check(PyStackRef_AsPyObjectBorrow(stackref)); -} - static inline void _PyThreadState_PushCStackRef(PyThreadState *tstate, _PyCStackRef *ref) { @@ -792,7 +807,7 @@ static inline int _Py_TryIncrefCompareStackRef(PyObject **src, PyObject *op, _PyStackRef *out) { if (_PyObject_HasDeferredRefcount(op)) { - *out = (_PyStackRef){ .bits = (uintptr_t)op | Py_TAG_DEFERRED }; + *out = (_PyStackRef){ .bits = (uintptr_t)op | Py_TAG_REFCNT }; return 1; } if (_Py_TryIncrefCompare(src, op)) { @@ -832,6 +847,18 @@ _Py_TryXGetStackRef(PyObject **src, _PyStackRef *out) } \ } while (0) +static inline void +_PyStackRef_CloseStack(_PyStackRef *arguments, int total_args) +{ + // arguments is a pointer into the GC visible stack, + // so we must NULL out values as we clear them. + for (int i = total_args-1; i >= 0; i--) { + _PyStackRef tmp = arguments[i]; + arguments[i] = PyStackRef_NULL; + PyStackRef_CLOSE(tmp); + } +} + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stats.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stats.h index 24f239a21..850e6ea45 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stats.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_stats.h @@ -15,39 +15,56 @@ extern "C" { #include "pycore_bitutils.h" // _Py_bit_length -#define STAT_INC(opname, name) do { if (_Py_stats) _Py_stats->opcode_stats[opname].specialization.name++; } while (0) -#define STAT_DEC(opname, name) do { if (_Py_stats) _Py_stats->opcode_stats[opname].specialization.name--; } while (0) -#define OPCODE_EXE_INC(opname) do { if (_Py_stats) _Py_stats->opcode_stats[opname].execution_count++; } while (0) -#define CALL_STAT_INC(name) do { if (_Py_stats) _Py_stats->call_stats.name++; } while (0) -#define OBJECT_STAT_INC(name) do { if (_Py_stats) _Py_stats->object_stats.name++; } while (0) -#define OBJECT_STAT_INC_COND(name, cond) \ - do { if (_Py_stats && cond) _Py_stats->object_stats.name++; } while (0) -#define EVAL_CALL_STAT_INC(name) do { if (_Py_stats) _Py_stats->call_stats.eval_calls[name]++; } while (0) -#define EVAL_CALL_STAT_INC_IF_FUNCTION(name, callable) \ - do { if (_Py_stats && PyFunction_Check(callable)) _Py_stats->call_stats.eval_calls[name]++; } while (0) -#define GC_STAT_ADD(gen, name, n) do { if (_Py_stats) _Py_stats->gc_stats[(gen)].name += (n); } while (0) -#define OPT_STAT_INC(name) do { if (_Py_stats) _Py_stats->optimization_stats.name++; } while (0) -#define OPT_STAT_ADD(name, n) do { if (_Py_stats) _Py_stats->optimization_stats.name += (n); } while (0) -#define UOP_STAT_INC(opname, name) do { if (_Py_stats) { assert(opname < 512); _Py_stats->optimization_stats.opcode[opname].name++; } } while (0) -#define UOP_PAIR_INC(uopcode, lastuop) \ - do { \ - if (lastuop && _Py_stats) { \ - _Py_stats->optimization_stats.opcode[lastuop].pair_count[uopcode]++; \ - } \ - lastuop = uopcode; \ +#define STAT_INC(opname, name) _Py_STATS_EXPR(opcode_stats[opname].specialization.name++) +#define STAT_DEC(opname, name) _Py_STATS_EXPR(opcode_stats[opname].specialization.name--) +#define OPCODE_EXE_INC(opname) _Py_STATS_EXPR(opcode_stats[opname].execution_count++) +#define CALL_STAT_INC(name) _Py_STATS_EXPR(call_stats.name++) +#define OBJECT_STAT_INC(name) _Py_STATS_EXPR(object_stats.name++) +#define OBJECT_STAT_INC_COND(name, cond) _Py_STATS_COND_EXPR(cond, object_stats.name++) +#define EVAL_CALL_STAT_INC(name) _Py_STATS_EXPR(call_stats.eval_calls[name]++) +#define EVAL_CALL_STAT_INC_IF_FUNCTION(name, callable) _Py_STATS_COND_EXPR(PyFunction_Check(callable), call_stats.eval_calls[name]++) +#define GC_STAT_ADD(gen, name, n) _Py_STATS_EXPR(gc_stats[(gen)].name += (n)) +#define OPT_STAT_INC(name) _Py_STATS_EXPR(optimization_stats.name++) +#define OPT_STAT_ADD(name, n) _Py_STATS_EXPR(optimization_stats.name += (n)) +#define UOP_STAT_INC(opname, name) \ + do { \ + PyStats *s = _PyStats_GET(); \ + if (s) { \ + assert(opname < 512); \ + s->optimization_stats.opcode[opname].name++; \ + } \ + } while (0) +#define UOP_PAIR_INC(uopcode, lastuop) \ + do { \ + PyStats *s = _PyStats_GET(); \ + if (lastuop && s) { \ + s->optimization_stats.opcode[lastuop].pair_count[uopcode]++; \ + } \ + lastuop = uopcode; \ } while (0) -#define OPT_UNSUPPORTED_OPCODE(opname) do { if (_Py_stats) _Py_stats->optimization_stats.unsupported_opcode[opname]++; } while (0) -#define OPT_ERROR_IN_OPCODE(opname) do { if (_Py_stats) _Py_stats->optimization_stats.error_in_opcode[opname]++; } while (0) +#define OPT_UNSUPPORTED_OPCODE(opname) _Py_STATS_EXPR(optimization_stats.unsupported_opcode[opname]++) +#define OPT_ERROR_IN_OPCODE(opname) _Py_STATS_EXPR(optimization_stats.error_in_opcode[opname]++) #define OPT_HIST(length, name) \ do { \ - if (_Py_stats) { \ + PyStats *s = _PyStats_GET(); \ + if (s) { \ int bucket = _Py_bit_length(length >= 1 ? length - 1 : 0); \ bucket = (bucket >= _Py_UOP_HIST_SIZE) ? _Py_UOP_HIST_SIZE - 1 : bucket; \ - _Py_stats->optimization_stats.name[bucket]++; \ + s->optimization_stats.name[bucket]++; \ } \ } while (0) -#define RARE_EVENT_STAT_INC(name) do { if (_Py_stats) _Py_stats->rare_event_stats.name++; } while (0) -#define OPCODE_DEFERRED_INC(opname) do { if (_Py_stats && opcode == opname) _Py_stats->opcode_stats[opname].specialization.deferred++; } while (0) +#define RARE_EVENT_STAT_INC(name) _Py_STATS_EXPR(rare_event_stats.name++) +#define OPCODE_DEFERRED_INC(opname) _Py_STATS_COND_EXPR(opcode==opname, opcode_stats[opname].specialization.deferred++) + +#ifdef Py_GIL_DISABLED +#define FT_STAT_MUTEX_SLEEP_INC() _Py_STATS_EXPR(ft_stats.mutex_sleeps++) +#define FT_STAT_QSBR_POLL_INC() _Py_STATS_EXPR(ft_stats.qsbr_polls++) +#define FT_STAT_WORLD_STOP_INC() _Py_STATS_EXPR(ft_stats.world_stops++) +#else +#define FT_STAT_MUTEX_SLEEP_INC() +#define FT_STAT_QSBR_POLL_INC() +#define FT_STAT_WORLD_STOP_INC() +#endif // Export for '_opcode' shared extension PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void); @@ -71,6 +88,9 @@ PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void); #define OPT_HIST(length, name) ((void)0) #define RARE_EVENT_STAT_INC(name) ((void)0) #define OPCODE_DEFERRED_INC(opname) ((void)0) +#define FT_STAT_MUTEX_SLEEP_INC() +#define FT_STAT_QSBR_POLL_INC() +#define FT_STAT_WORLD_STOP_INC() #endif // !Py_STATS @@ -90,6 +110,11 @@ PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void); RARE_EVENT_INTERP_INC(interp, name); \ } while (0); \ +PyStatus _PyStats_InterpInit(PyInterpreterState *); +bool _PyStats_ThreadInit(PyInterpreterState *, _PyThreadStateImpl *); +void _PyStats_ThreadFini(_PyThreadStateImpl *); +void _PyStats_Attach(_PyThreadStateImpl *); +void _PyStats_Detach(_PyThreadStateImpl *); #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_strhex.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_strhex.h index 225f42391..656acae96 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_strhex.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_strhex.h @@ -10,28 +10,24 @@ extern "C" { // Returns a str() containing the hex representation of argbuf. // Export for '_hashlib' shared extension. -PyAPI_FUNC(PyObject*) _Py_strhex(const - char* argbuf, - const Py_ssize_t arglen); +PyAPI_FUNC(PyObject *) _Py_strhex(const char *argbuf, Py_ssize_t arglen); // Returns a bytes() containing the ASCII hex representation of argbuf. -extern PyObject* _Py_strhex_bytes( - const char* argbuf, - const Py_ssize_t arglen); +extern PyObject *_Py_strhex_bytes(const char *argbuf, Py_ssize_t arglen); // These variants include support for a separator between every N bytes: -extern PyObject* _Py_strhex_with_sep( - const char* argbuf, - const Py_ssize_t arglen, - PyObject* sep, - const int bytes_per_group); +extern PyObject *_Py_strhex_with_sep( + const char *argbuf, + Py_ssize_t arglen, + PyObject *sep, + Py_ssize_t bytes_per_group); // Export for 'binascii' shared extension -PyAPI_FUNC(PyObject*) _Py_strhex_bytes_with_sep( - const char* argbuf, - const Py_ssize_t arglen, - PyObject* sep, - const int bytes_per_group); +PyAPI_FUNC(PyObject *) _Py_strhex_bytes_with_sep( + const char *argbuf, + Py_ssize_t arglen, + PyObject *sep, + Py_ssize_t bytes_per_group); #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_symtable.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_symtable.h index 98099b4a4..c650a94a1 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_symtable.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_symtable.h @@ -90,6 +90,7 @@ typedef struct _symtable_entry { PyObject *ste_id; /* int: key in ste_table->st_blocks */ PyObject *ste_symbols; /* dict: variable names to flags */ PyObject *ste_name; /* string: name of current block */ + PyObject *ste_function_name; /* string or NULL: for annotation blocks: name of the corresponding functions */ PyObject *ste_varnames; /* list of function parameters */ PyObject *ste_children; /* list of child blocks */ PyObject *ste_directives;/* locations of global and nonlocal statements */ @@ -126,6 +127,7 @@ typedef struct _symtable_entry { unsigned ste_method : 1; /* true if block is a function block defined in class scope */ unsigned ste_has_conditional_annotations : 1; /* true if block has conditionally executed annotations */ unsigned ste_in_conditional_block : 1; /* set while we are inside a conditionally executed block */ + unsigned ste_in_try_block : 1; /* set while we are inside a try/except block */ unsigned ste_in_unevaluated_annotation : 1; /* set while we are processing an annotation that will not be evaluated */ int ste_comp_iter_expr; /* non-zero if visiting a comprehension range expression */ _Py_SourceLocation ste_loc; /* source location of block */ @@ -151,7 +153,12 @@ extern int _PySymtable_LookupOptional(struct symtable *, void *, PySTEntryObject extern void _PySymtable_Free(struct symtable *); extern PyObject *_Py_MaybeMangle(PyObject *privateobj, PySTEntryObject *ste, PyObject *name); -extern PyObject* _Py_Mangle(PyObject *p, PyObject *name); + +// Export for '_pickle' shared extension +PyAPI_FUNC(PyObject *) +_Py_Mangle(PyObject *, PyObject *); +PyAPI_FUNC(int) +_Py_IsPrivateName(PyObject *); /* Flags for def-use information */ @@ -188,7 +195,8 @@ extern struct symtable* _Py_SymtableStringObjectFlags( const char *str, PyObject *filename, int start, - PyCompilerFlags *flags); + PyCompilerFlags *flags, + PyObject *module); int _PyFuture_FromAST( struct _mod * mod, diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_sysmodule.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_sysmodule.h index 008a2da0d..347b0a7a7 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_sysmodule.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_sysmodule.h @@ -8,11 +8,6 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -PyAPI_FUNC(int) _PySys_GetOptionalAttr(PyObject *, PyObject **); -PyAPI_FUNC(int) _PySys_GetOptionalAttrString(const char *, PyObject **); -PyAPI_FUNC(PyObject *) _PySys_GetRequiredAttr(PyObject *); -PyAPI_FUNC(PyObject *) _PySys_GetRequiredAttrString(const char *); - // Export for '_pickle' shared extension PyAPI_FUNC(size_t) _PySys_GetSizeOf(PyObject *); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_time.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_time.h index 23312471c..b671225ca 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_time.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_time.h @@ -147,11 +147,6 @@ extern int _PyTime_FromSecondsDouble( // Clamp to [PyTime_MIN; PyTime_MAX] on overflow. extern PyTime_t _PyTime_FromMicrosecondsClamp(PyTime_t us); -// Create a timestamp from a Python int object (number of nanoseconds). -// Export for '_lsprof' shared extension. -PyAPI_FUNC(int) _PyTime_FromLong(PyTime_t *t, - PyObject *obj); - // Convert a number of seconds (Python float or int) to a timestamp. // Raise an exception and return -1 on error, return 0 on success. // Export for '_socket' shared extension. @@ -182,10 +177,6 @@ extern PyTime_t _PyTime_As100Nanoseconds(PyTime_t t, _PyTime_round_t round); #endif -// Convert a timestamp (number of nanoseconds) as a Python int object. -// Export for '_testinternalcapi' shared extension. -PyAPI_FUNC(PyObject*) _PyTime_AsLong(PyTime_t t); - #ifndef MS_WINDOWS // Create a timestamp from a timeval structure. // Raise an exception and return -1 on overflow, return 0 on success. diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_traceback.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_traceback.h index 8357cce9d..e016afaa5 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_traceback.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_traceback.h @@ -14,55 +14,6 @@ PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int, int *, P // Export for 'pyexact' shared extension PyAPI_FUNC(void) _PyTraceback_Add(const char *, const char *, int); -/* Write the Python traceback into the file 'fd'. For example: - - Traceback (most recent call first): - File "xxx", line xxx in - File "xxx", line xxx in - ... - File "xxx", line xxx in - - This function is written for debug purpose only, to dump the traceback in - the worst case: after a segmentation fault, at fatal error, etc. That's why, - it is very limited. Strings are truncated to 100 characters and encoded to - ASCII with backslashreplace. It doesn't write the source code, only the - function name, filename and line number of each frame. Write only the first - 100 frames: if the traceback is truncated, write the line " ...". - - This function is signal safe. */ - -extern void _Py_DumpTraceback( - int fd, - PyThreadState *tstate); - -/* Write the traceback of all threads into the file 'fd'. current_thread can be - NULL. - - Return NULL on success, or an error message on error. - - This function is written for debug purpose only. It calls - _Py_DumpTraceback() for each thread, and so has the same limitations. It - only write the traceback of the first 100 threads: write "..." if there are - more threads. - - If current_tstate is NULL, the function tries to get the Python thread state - of the current thread. It is not an error if the function is unable to get - the current Python thread state. - - If interp is NULL, the function tries to get the interpreter state from - the current Python thread state, or from - _PyGILState_GetInterpreterStateUnsafe() in last resort. - - It is better to pass NULL to interp and current_tstate, the function tries - different options to retrieve this information. - - This function is signal safe. */ - -extern const char* _Py_DumpTracebackThreads( - int fd, - PyInterpreterState *interp, - PyThreadState *current_tstate); - /* Write a Unicode object into the file descriptor fd. Encode the string to ASCII using the backslashreplace error handler. @@ -85,7 +36,8 @@ extern void _Py_DumpHexadecimal( uintptr_t value, Py_ssize_t width); -extern PyObject* _PyTraceBack_FromFrame( +// Exported for external JIT support +PyAPI_FUNC(PyObject *) _PyTraceBack_FromFrame( PyObject *tb_next, PyFrameObject *frame); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tracemalloc.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tracemalloc.h index 41e1c91be..0522b24cd 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tracemalloc.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tracemalloc.h @@ -33,8 +33,8 @@ struct _PyTraceMalloc_Config { }; -/* Pack the frame_t structure to reduce the memory footprint on 64-bit - architectures: 12 bytes instead of 16. */ +/* Pack the tracemalloc_frame and tracemalloc_traceback structures to reduce + the memory footprint on 64-bit architectures: 12 bytes instead of 16. */ #if defined(_MSC_VER) #pragma pack(push, 4) #endif @@ -44,23 +44,28 @@ struct __attribute__((packed)) #endif tracemalloc_frame { - /* filename cannot be NULL: "" is used if the Python frame - filename is NULL */ - PyObject *filename; + /* Interned NUL terminated UTF-8 (surrogatepass) string. + Cannot be NULL: "" is used if the Python frame filename + cannot be captured. */ + const char *filename; unsigned int lineno; }; -#ifdef _MSC_VER -#pragma pack(pop) -#endif -struct tracemalloc_traceback { +struct +#ifdef __GNUC__ +__attribute__((packed)) +#endif +tracemalloc_traceback { Py_uhash_t hash; /* Number of frames stored */ uint16_t nframe; /* Total number of frames the traceback had */ uint16_t total_nframe; - struct tracemalloc_frame frames[1]; + struct tracemalloc_frame frames[]; }; +#ifdef _MSC_VER +#pragma pack(pop) +#endif struct _tracemalloc_runtime_state { @@ -81,7 +86,7 @@ struct _tracemalloc_runtime_state { Protected by TABLES_LOCK(). */ size_t peak_traced_memory; /* Hash table used as a set to intern filenames: - PyObject* => PyObject*. + char* (NUL terminated UTF-8 string) => NULL. Protected by the TABLES_LOCK(). */ _Py_hashtable_t *filenames; /* Buffer to store a new traceback in traceback_new(). @@ -98,7 +103,7 @@ struct _tracemalloc_runtime_state { Protected by TABLES_LOCK(). */ _Py_hashtable_t *domains; - struct tracemalloc_traceback empty_traceback; + struct tracemalloc_traceback *empty_traceback; Py_tss_t reentrant_key; }; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tstate.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tstate.h index 87f59c274..c11d28187 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tstate.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tstate.h @@ -10,9 +10,11 @@ extern "C" { #include "pycore_brc.h" // struct _brc_thread_state #include "pycore_freelist_state.h" // struct _Py_freelists +#include "pycore_interpframe_structs.h" // _PyInterpreterFrame #include "pycore_mimalloc.h" // struct _mimalloc_thread_state #include "pycore_qsbr.h" // struct qsbr - +#include "pycore_uop.h" // struct _PyUOpInstruction +#include "pycore_structs.h" #ifdef Py_GIL_DISABLED struct _gc_thread_state { @@ -21,6 +23,7 @@ struct _gc_thread_state { }; #endif + // Every PyThreadState is actually allocated as a _PyThreadStateImpl. The // PyThreadState fields are exposed as part of the C API, although most fields // are intended to be private. The _PyThreadStateImpl fields not exposed. @@ -28,6 +31,10 @@ typedef struct _PyThreadStateImpl { // semi-public fields are in PyThreadState. PyThreadState base; + // Embedded base frame - sentinel at the bottom of the frame stack. + // Used by profiling/sampling to detect incomplete stack traces. + _PyInterpreterFrame base_frame; + // The reference count field is used to synchronize deallocation of the // thread state during runtime finalization. Py_ssize_t refcount; @@ -37,9 +44,20 @@ typedef struct _PyThreadStateImpl { uintptr_t c_stack_soft_limit; uintptr_t c_stack_hard_limit; + // PyUnstable_ThreadState_ResetStackProtection() values + uintptr_t c_stack_init_base; + uintptr_t c_stack_init_top; + PyObject *asyncio_running_loop; // Strong reference PyObject *asyncio_running_task; // Strong reference + // Distinguishes between yield and return from PyEval_EvalFrame(). + // See gen_send_ex2() in Objects/genobject.c + enum { + GENERATOR_RETURN = 0, + GENERATOR_YIELD = 1, + } generator_return_kind; + /* Head of circular linked-list of all tasks which are instances of `asyncio.Task` or subclasses of it used in `asyncio.all_tasks`. */ @@ -70,15 +88,23 @@ typedef struct _PyThreadStateImpl { // When >1, code objects do not immortalize their non-string constants. int suppress_co_const_immortalization; + +#ifdef Py_STATS + // per-thread stats, will be merged into interp->pystats_struct + PyStats *pystats_struct; // allocated by _PyStats_ThreadInit() #endif +#endif // Py_GIL_DISABLED + #if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED) Py_ssize_t reftotal; // this thread's total refcount operations #endif +#if _Py_TIER2 + struct _PyJitTracerState *jit_tracer_state; +#endif - // PyUnstable_ThreadState_ResetStackProtection() values - uintptr_t c_stack_init_base; - uintptr_t c_stack_init_top; + // Reused for the thread's lifetime; owns placeholders while they resolve. + PyObject *lazy_imports; #ifdef Py_GIL_DISABLED // gh-144438: Add padding to ensure that the fields above don't share a diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tuple.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tuple.h index acf1bec46..bf80f9639 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tuple.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_tuple.h @@ -21,11 +21,17 @@ extern PyStatus _PyTuple_InitGlobalObjects(PyInterpreterState *); /* other API */ +PyAPI_FUNC(void) _PyStolenTuple_Free(PyObject *self); + #define _PyTuple_ITEMS(op) _Py_RVALUE(_PyTuple_CAST(op)->ob_item) -PyAPI_FUNC(PyObject *)_PyTuple_FromArray(PyObject *const *, Py_ssize_t); PyAPI_FUNC(PyObject *)_PyTuple_FromStackRefStealOnSuccess(const union _PyStackRef *, Py_ssize_t); PyAPI_FUNC(PyObject *)_PyTuple_FromArraySteal(PyObject *const *, Py_ssize_t); +PyAPI_FUNC(PyObject *) _PyTuple_BinarySlice(PyObject *, PyObject *, PyObject *); +PyAPI_FUNC(PyObject *) _PyTuple_Concat(PyObject *, PyObject *); + +PyAPI_FUNC(PyObject *) _PyTuple_FromPair(PyObject *, PyObject *); +PyAPI_FUNC(PyObject *) _PyTuple_FromPairSteal(PyObject *, PyObject *); typedef struct { PyObject_HEAD diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_typeobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_typeobject.h index ba66364d3..785b77d3e 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_typeobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_typeobject.h @@ -10,6 +10,7 @@ extern "C" { #include "pycore_interp_structs.h" // managed_static_type_state #include "pycore_moduleobject.h" // PyModuleObject +#include "pycore_structs.h" // _PyStackRef /* state */ @@ -25,6 +26,7 @@ extern "C" { #define _Py_TYPE_VERSION_BYTEARRAY 9 #define _Py_TYPE_VERSION_BYTES 10 #define _Py_TYPE_VERSION_COMPLEX 11 +#define _Py_TYPE_VERSION_FROZENDICT 12 #define _Py_TYPE_VERSION_NEXT 16 @@ -40,6 +42,7 @@ extern void _PyTypes_FiniTypes(PyInterpreterState *); extern void _PyTypes_FiniExtTypes(PyInterpreterState *interp); extern void _PyTypes_Fini(PyInterpreterState *); extern void _PyTypes_AfterFork(void); +extern void _PyTypes_FiniCachedDescriptors(PyInterpreterState *); static inline PyObject ** _PyStaticType_GET_WEAKREFS_LISTPTR(managed_static_type_state *state) @@ -57,7 +60,8 @@ extern void _PyStaticType_FiniBuiltin( extern void _PyStaticType_ClearWeakRefs( PyInterpreterState *interp, PyTypeObject *type); -extern managed_static_type_state * _PyStaticType_GetState( +// Exported for external JIT support +PyAPI_FUNC(managed_static_type_state *) _PyStaticType_GetState( PyInterpreterState *interp, PyTypeObject *type); @@ -89,8 +93,10 @@ _PyType_GetModuleState(PyTypeObject *type) // function PyAPI_FUNC(PyObject *) _PyType_GetDict(PyTypeObject *); +PyAPI_FUNC(PyObject *) _PyType_LookupSubclasses(PyTypeObject *); +PyAPI_FUNC(PyObject *) _PyType_InitSubclasses(PyTypeObject *); + extern PyObject * _PyType_GetBases(PyTypeObject *type); -extern PyObject * _PyType_GetMRO(PyTypeObject *type); extern PyObject* _PyType_GetSubclasses(PyTypeObject *); extern int _PyType_HasSubclasses(PyTypeObject *); @@ -108,17 +114,25 @@ _PyType_IsReady(PyTypeObject *type) extern PyObject* _Py_type_getattro_impl(PyTypeObject *type, PyObject *name, int *suppress_missing_attribute); extern PyObject* _Py_type_getattro(PyObject *type, PyObject *name); +extern _PyStackRef _Py_type_getattro_stackref(PyTypeObject *type, PyObject *name, + int *suppress_missing_attribute); extern PyObject* _Py_BaseObject_RichCompare(PyObject* self, PyObject* other, int op); extern PyObject* _Py_slot_tp_getattro(PyObject *self, PyObject *name); extern PyObject* _Py_slot_tp_getattr_hook(PyObject *self, PyObject *name); +extern int _PyType_HasSlotTpIternext(PyTypeObject *type); + extern PyTypeObject _PyBufferWrapper_Type; PyAPI_FUNC(PyObject*) _PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *meth_found); +extern PyObject *_PySuper_LookupDescr(PyTypeObject *su_type, + PyTypeObject *su_obj_type, + PyObject *name); + extern PyObject* _PyType_GetFullyQualifiedName(PyTypeObject *type, char sep); // Perform the following operation, in a thread-safe way when required by the @@ -134,7 +148,6 @@ extern int _PyType_AddMethod(PyTypeObject *, PyMethodDef *); extern void _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask, unsigned long flags); -extern unsigned int _PyType_GetVersionForCurrentState(PyTypeObject *tp); PyAPI_FUNC(void) _PyType_SetVersion(PyTypeObject *tp, unsigned int version); PyTypeObject *_PyType_LookupByVersion(unsigned int version); @@ -146,8 +159,12 @@ typedef int (*_py_validate_type)(PyTypeObject *); // It will verify the ``ty`` through user-defined validation function ``validate``, // and if the validation is passed, it will set the ``tp_version`` as valid // tp_version_tag from the ``ty``. -extern int _PyType_Validate(PyTypeObject *ty, _py_validate_type validate, unsigned int *tp_version); -extern int _PyType_CacheGetItemForSpecialization(PyHeapTypeObject *ht, PyObject *descriptor, uint32_t tp_version); +// Exported for external JIT support +int _PyType_Validate(PyTypeObject *ty, _py_validate_type validate, unsigned int *tp_version); +int _PyType_CacheGetItemForSpecialization(PyHeapTypeObject *ht, PyObject *descriptor, uint32_t tp_version); + +// Precalculates count of non-unique slots and fills wrapperbase.name_count. +extern int _PyType_InitSlotDefs(PyInterpreterState *interp); // Like PyType_GetBaseByToken, but does not modify refcounts. // Cannot fail; arguments must be valid. diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodectype.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodectype.h new file mode 100644 index 000000000..523bdb56b --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodectype.h @@ -0,0 +1,25 @@ +#ifndef Py_INTERNAL_UNICODECTYPE_H +#define Py_INTERNAL_UNICODECTYPE_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +extern int _PyUnicode_ToLowerFull(Py_UCS4 ch, Py_UCS4 *res); +extern int _PyUnicode_ToTitleFull(Py_UCS4 ch, Py_UCS4 *res); +extern int _PyUnicode_ToUpperFull(Py_UCS4 ch, Py_UCS4 *res); +extern int _PyUnicode_ToFoldedFull(Py_UCS4 ch, Py_UCS4 *res); +extern int _PyUnicode_IsCaseIgnorable(Py_UCS4 ch); +extern int _PyUnicode_IsCased(Py_UCS4 ch); + +// Export for 'unicodedata' shared extension. +PyAPI_FUNC(int) _PyUnicode_IsXidStart(Py_UCS4 ch); +PyAPI_FUNC(int) _PyUnicode_IsXidContinue(Py_UCS4 ch); + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_UNICODECTYPE_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject.h index 3791b913c..74d84052a 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject.h @@ -11,16 +11,109 @@ extern "C" { #include "pycore_fileutils.h" // _Py_error_handler #include "pycore_ucnhash.h" // _PyUnicode_Name_CAPI -/* --- Characters Type APIs ----------------------------------------------- */ -extern int _PyUnicode_IsXidStart(Py_UCS4 ch); -extern int _PyUnicode_IsXidContinue(Py_UCS4 ch); -extern int _PyUnicode_ToLowerFull(Py_UCS4 ch, Py_UCS4 *res); -extern int _PyUnicode_ToTitleFull(Py_UCS4 ch, Py_UCS4 *res); -extern int _PyUnicode_ToUpperFull(Py_UCS4 ch, Py_UCS4 *res); -extern int _PyUnicode_ToFoldedFull(Py_UCS4 ch, Py_UCS4 *res); -extern int _PyUnicode_IsCaseIgnorable(Py_UCS4 ch); -extern int _PyUnicode_IsCased(Py_UCS4 ch); +// Maximum code point of Unicode 6.0: 0x10ffff (1,114,111). +#define _Py_MAX_UNICODE 0x10ffff + + +extern int _PyUnicode_IsModifiable(PyObject *unicode); +extern void _PyUnicodeWriter_InitWithBuffer( + _PyUnicodeWriter *writer, + PyObject *buffer); +extern PyObject* _PyUnicode_Result(PyObject *unicode); +extern int _PyUnicode_DecodeUTF8Writer( + _PyUnicodeWriter *writer, + const char *s, + Py_ssize_t size, + _Py_error_handler error_handler, + const char *errors, + Py_ssize_t *consumed); +extern PyObject* _PyUnicode_ResizeCompact( + PyObject *unicode, + Py_ssize_t length); +extern PyObject* _PyUnicode_GetEmpty(void); +PyAPI_FUNC(PyObject*) _PyUnicode_BinarySlice(PyObject *, PyObject *, PyObject *); + + +/* Generic helper macro to convert characters of different types. + from_type and to_type have to be valid type names, begin and end + are pointers to the source characters which should be of type + "from_type *". to is a pointer of type "to_type *" and points to the + buffer where the result characters are written to. */ +#define _PyUnicode_CONVERT_BYTES(from_type, to_type, begin, end, to) \ + do { \ + to_type *_to = (to_type *)(to); \ + const from_type *_iter = (const from_type *)(begin);\ + const from_type *_end = (const from_type *)(end);\ + Py_ssize_t n = (_end) - (_iter); \ + const from_type *_unrolled_end = \ + _iter + _Py_SIZE_ROUND_DOWN(n, 4); \ + while (_iter < (_unrolled_end)) { \ + _to[0] = (to_type) _iter[0]; \ + _to[1] = (to_type) _iter[1]; \ + _to[2] = (to_type) _iter[2]; \ + _to[3] = (to_type) _iter[3]; \ + _iter += 4; _to += 4; \ + } \ + while (_iter < (_end)) \ + *_to++ = (to_type) *_iter++; \ + } while (0) + + +static inline void +_PyUnicode_Fill(int kind, void *data, Py_UCS4 value, + Py_ssize_t start, Py_ssize_t length) +{ + assert(0 <= start); + switch (kind) { + case PyUnicode_1BYTE_KIND: { + assert(value <= 0xff); + Py_UCS1 ch = (unsigned char)value; + Py_UCS1 *to = (Py_UCS1 *)data + start; + memset(to, ch, length); + break; + } + case PyUnicode_2BYTE_KIND: { + assert(value <= 0xffff); + Py_UCS2 ch = (Py_UCS2)value; + Py_UCS2 *to = (Py_UCS2 *)data + start; + const Py_UCS2 *end = to + length; + for (; to < end; ++to) *to = ch; + break; + } + case PyUnicode_4BYTE_KIND: { + assert(value <= _Py_MAX_UNICODE); + Py_UCS4 ch = value; + Py_UCS4 * to = (Py_UCS4 *)data + start; + const Py_UCS4 *end = to + length; + for (; to < end; ++to) *to = ch; + break; + } + default: Py_UNREACHABLE(); + } +} + +static inline int +_PyUnicode_EnsureUnicode(PyObject *obj) +{ + if (!PyUnicode_Check(obj)) { + PyErr_Format(PyExc_TypeError, + "must be str, not %T", obj); + return -1; + } + return 0; +} + +static inline int +_PyUnicodeWriter_WriteCharInline(_PyUnicodeWriter *writer, Py_UCS4 ch) +{ + assert(ch <= _Py_MAX_UNICODE); + if (_PyUnicodeWriter_Prepare(writer, 1, ch) < 0) + return -1; + PyUnicode_WRITE(writer->kind, writer->data, writer->pos, ch); + writer->pos++; + return 0; +} /* --- Unicode API -------------------------------------------------------- */ @@ -82,12 +175,16 @@ extern int _PyUnicode_FormatAdvancedWriter( Py_ssize_t start, Py_ssize_t end); +/* PyUnicodeWriter_Format, with va_list instead of `...` */ +extern int _PyUnicodeWriter_FormatV( + PyUnicodeWriter *writer, + const char *format, + va_list vargs); + /* --- UTF-7 Codecs ------------------------------------------------------- */ extern PyObject* _PyUnicode_EncodeUTF7( PyObject *unicode, /* Unicode object */ - int base64SetO, /* Encode RFC2152 Set O characters in base64 */ - int base64WhiteSpace, /* Encode whitespace (sp, ht, nl, cr) in base64 */ const char *errors); /* error handling */ /* --- UTF-8 Codecs ------------------------------------------------------- */ @@ -211,14 +308,6 @@ PyAPI_FUNC(PyObject*) _PyUnicode_JoinArray( Py_ssize_t seqlen ); -/* Test whether a unicode is equal to ASCII identifier. Return 1 if true, - 0 otherwise. The right argument must be ASCII identifier. - Any error occurs inside will be cleared before return. */ -extern int _PyUnicode_EqualToASCIIId( - PyObject *left, /* Left string */ - _Py_Identifier *right /* Right identifier */ - ); - // Test whether a unicode is equal to ASCII string. Return 1 if true, // 0 otherwise. The right argument must be ASCII-encoded string. // Any error occurs inside will be cleared before return. @@ -236,23 +325,9 @@ extern PyObject* _PyUnicode_XStrip( ); -/* Using explicit passed-in values, insert the thousands grouping - into the string pointed to by buffer. For the argument descriptions, - see Objects/stringlib/localeutil.h */ -extern Py_ssize_t _PyUnicode_InsertThousandsGrouping( - _PyUnicodeWriter *writer, - Py_ssize_t n_buffer, - PyObject *digits, - Py_ssize_t d_pos, - Py_ssize_t n_digits, - Py_ssize_t min_width, - const char *grouping, - PyObject *thousands_sep, - Py_UCS4 *maxchar, - int forward); - /* Dedent a string. - Behaviour is expected to be an exact match of `textwrap.dedent`. + Intended to dedent Python source. Unlike `textwrap.dedent`, this + only supports spaces and tabs and doesn't normalize empty lines. Return a new reference on success, NULL with exception set on error. */ extern PyObject* _PyUnicode_Dedent(PyObject *unicode); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject_generated.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject_generated.h index c3ecdf64c..ddda6b467 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject_generated.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_unicodeobject_generated.h @@ -12,10 +12,50 @@ extern "C" { static inline void _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { PyObject *string; + string = &_Py_ID(AGEN_CLOSED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(AGEN_CREATED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(AGEN_RUNNING); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(AGEN_SUSPENDED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(CANCELLED); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_CLOSED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_CREATED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_RUNNING); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(CORO_SUSPENDED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(Emax); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(Emin); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(FINISHED); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -24,6 +64,22 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_CLOSED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_CREATED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_RUNNING); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(GEN_SUSPENDED); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(JSONDecodeError); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -400,6 +456,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(__lazy_import__); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(__lazy_modules__); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(__le__); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -784,6 +848,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(_find_and_load_lazy_submodule); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(_fix_up_module); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -912,15 +980,15 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(after_in_child); + string = &_Py_ID(adobe); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(after_in_parent); + string = &_Py_ID(after_in_child); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(aggregate_class); + string = &_Py_ID(after_in_parent); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); @@ -936,6 +1004,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(all_interpreters); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(all_threads); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -944,6 +1016,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(alphabet); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(any); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -992,10 +1068,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(authorizer_callback); - _PyUnicode_InternStatic(interp, &string); - assert(_PyUnicode_CheckConsistency(string, 1)); - assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(autocommit); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1032,6 +1104,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(blocking); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(bound); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1100,6 +1176,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(c_stack); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(cache_frames); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(cached_datetime_module); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1128,6 +1212,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(callable); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(callback); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1136,10 +1224,22 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(canonical); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(capath); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(capitals); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(capture_features); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(category); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1152,10 +1252,18 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(chain); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(check_same_thread); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(clamp); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(clear); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1256,6 +1364,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(collector); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(command); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1268,6 +1380,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(compression); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(config); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(consts); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1308,6 +1428,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(ctx); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(cwd); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1324,10 +1448,18 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(date); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(day); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(days); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(debug); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1376,10 +1508,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(dictcomp); - _PyUnicode_InternStatic(interp, &string); - assert(_PyUnicode_CheckConsistency(string, 1)); - assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(difference_update); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1436,6 +1564,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(duration_sec); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(eager_start); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1448,6 +1580,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(emptyerror); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(encode); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1484,6 +1620,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(error_rate); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(errors); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1496,10 +1636,22 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(exc); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(exc_tb); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(exc_type); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(exc_val); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(exc_value); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1516,6 +1668,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(exit); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(exp); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1540,6 +1696,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(fallback); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(false); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1628,6 +1788,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(foldspaces); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(follow_symlinks); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1644,6 +1808,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(free_threaded); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(from_param); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1664,6 +1832,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(fullerror); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(func); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1672,11 +1844,11 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(generation); + string = &_Py_ID(gc); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(genexpr); + string = &_Py_ID(generation); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); @@ -1760,6 +1932,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(hours); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(id); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1776,10 +1952,18 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(ignorechars); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(imag); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(implieslink); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(importlib); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1788,6 +1972,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(include_aliases); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(incoming); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -1972,15 +2160,15 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(kwdefaults); + string = &_Py_ID(kwargs); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(label); + string = &_Py_ID(kwdefaults); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(lambda); + string = &_Py_ID(label); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); @@ -2016,6 +2204,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(legacy); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(len); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2044,10 +2236,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(listcomp); - _PyUnicode_InternStatic(interp, &string); - assert(_PyUnicode_CheckConsistency(string, 1)); - assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(little); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2080,6 +2268,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(mask); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(match); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2088,6 +2280,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(max_threads); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(maxdigits); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2104,6 +2300,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(maxsize); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(maxsplit); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2140,6 +2340,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(microseconds); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(milliseconds); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2148,6 +2352,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(minutes); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(missed_samples); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(mod); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2168,6 +2380,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(modulo); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(month); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2188,10 +2404,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(n_arg); - _PyUnicode_InternStatic(interp, &string); - assert(_PyUnicode_CheckConsistency(string, 1)); - assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(n_fields); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2220,7 +2432,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(narg); + string = &_Py_ID(native); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); @@ -2328,6 +2540,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(opcodes); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(open); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2356,6 +2572,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(other); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(out_fd); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2376,6 +2596,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(pad); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(padded); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(pages); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2424,6 +2652,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(pointer_bits); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(policy); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2444,6 +2676,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(prec); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(preserve_exc); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(print_file_and_line); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2456,7 +2696,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(progress_handler); + string = &_Py_ID(progress_callback); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); @@ -2480,10 +2720,22 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(qid); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(qualname); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(query); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(queuetype); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(quotetabs); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2528,6 +2780,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(recursive); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(reducer_override); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2548,6 +2804,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(repeat); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(repl); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2556,6 +2816,18 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(repr); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(reqrefs); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(require_ready); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(reserved); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2568,6 +2840,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(restrict); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(return); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2580,10 +2856,22 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(rounding); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(salt); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(sample_interval_us); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(sample_rate); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(sched_priority); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2600,6 +2888,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(seconds); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(security_attributes); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2644,10 +2936,6 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(setcomp); - _PyUnicode_InternStatic(interp, &string); - assert(_PyUnicode_CheckConsistency(string, 1)); - assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(setpgroup); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2672,6 +2960,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(shared); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(short); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(show_cmd); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2680,6 +2976,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(signum); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(size); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2692,6 +2992,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(skip_non_matching_threads); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(sleep); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2724,6 +3028,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(stack_frames); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(stacklevel); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2732,10 +3040,18 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(start_time_us); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(statement); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(stats); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(status); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2804,6 +3120,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(take_bytes); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(target); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2848,6 +3168,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(third); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(threading); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2856,6 +3180,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(time); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(timeout); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2868,19 +3196,27 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(timetuple); + string = &_Py_ID(timespec); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(timeunit); + string = &_Py_ID(timestamp); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(top); + string = &_Py_ID(timestamp_us); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(timetuple); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(timeunit); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); - string = &_Py_ID(trace_callback); + string = &_Py_ID(top); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); @@ -2896,6 +3232,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(traps); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(true); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2936,6 +3276,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(unboundop); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(unlink); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2944,6 +3288,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(updates); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(uri); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2996,6 +3344,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(weeks); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(which); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -3008,6 +3360,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(wrapcol); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(writable); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -3068,6 +3424,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_STR(gc); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_STR(anon_null); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -3092,6 +3452,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_STR(native); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_STR(anon_setcomp); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop.h new file mode 100644 index 000000000..e7f0d2c21 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop.h @@ -0,0 +1,73 @@ +#ifndef Py_CORE_UOP_H +#define Py_CORE_UOP_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include +/* Depending on the format, + * the 32 bits between the oparg and operand are: + * UOP_FORMAT_TARGET: + * uint32_t target; + * UOP_FORMAT_JUMP + * uint16_t jump_target; + * uint16_t error_target; + */ +typedef struct _PyUOpInstruction{ + uint16_t opcode:15; + uint16_t format:1; + uint16_t oparg; + union { + uint32_t target; + struct { + uint16_t jump_target; + uint16_t error_target; + }; + }; + uint64_t operand0; // A cache entry + uint64_t operand1; +#ifdef Py_STATS + int32_t fitness; + uint64_t execution_count; +#endif +} _PyUOpInstruction; + +// Fitness is the target length of the trace we translate initially. The uop +// buffer has a small amount of extra space for entry/loop-closing overhead. +#if defined(Py_DEBUG) && defined(_Py_JIT) + // With asserts, the stencils are a lot larger +#define FITNESS_INITIAL 1000 +#else +#define FITNESS_INITIAL 2500 +#endif + +#define UOP_TRACE_BUFFER_OVERHEAD 10 +#define UOP_MAX_TRACE_LENGTH (FITNESS_INITIAL + UOP_TRACE_BUFFER_OVERHEAD) + +/* Bloom filter with m = 256 + * https://en.wikipedia.org/wiki/Bloom_filter */ +#ifdef HAVE_GCC_UINT128_T +#define _Py_BLOOM_FILTER_WORDS 2 +typedef __uint128_t _Py_bloom_filter_word_t; +#else +#define _Py_BLOOM_FILTER_WORDS 4 +typedef uint64_t _Py_bloom_filter_word_t; +#endif + +#define _Py_BLOOM_FILTER_BITS_PER_WORD \ + ((int)(sizeof(_Py_bloom_filter_word_t) * 8)) +#define _Py_BLOOM_FILTER_WORD_SHIFT \ + ((sizeof(_Py_bloom_filter_word_t) == 16) ? 7 : 6) + +typedef struct { + _Py_bloom_filter_word_t bits[_Py_BLOOM_FILTER_WORDS]; +} _PyBloomFilter; + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_UOP_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_ids.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_ids.h index d3d2b716e..7c44c0a43 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_ids.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_ids.h @@ -11,24 +11,42 @@ extern "C" { #define _EXIT_TRACE 300 #define _SET_IP 301 -#define _BINARY_OP 302 -#define _BINARY_OP_ADD_FLOAT 303 -#define _BINARY_OP_ADD_INT 304 -#define _BINARY_OP_ADD_UNICODE 305 -#define _BINARY_OP_EXTEND 306 -#define _BINARY_OP_INPLACE_ADD_UNICODE 307 -#define _BINARY_OP_MULTIPLY_FLOAT 308 -#define _BINARY_OP_MULTIPLY_INT 309 -#define _BINARY_OP_SUBSCR_CHECK_FUNC 310 -#define _BINARY_OP_SUBSCR_DICT 311 -#define _BINARY_OP_SUBSCR_INIT_CALL 312 -#define _BINARY_OP_SUBSCR_LIST_INT 313 -#define _BINARY_OP_SUBSCR_LIST_SLICE 314 -#define _BINARY_OP_SUBSCR_STR_INT 315 -#define _BINARY_OP_SUBSCR_TUPLE_INT 316 -#define _BINARY_OP_SUBTRACT_FLOAT 317 -#define _BINARY_OP_SUBTRACT_INT 318 -#define _BINARY_SLICE 319 +#define _ALLOCATE_OBJECT 302 +#define _BINARY_OP 303 +#define _BINARY_OP_ADD_FLOAT 304 +#define _BINARY_OP_ADD_FLOAT_INPLACE 305 +#define _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT 306 +#define _BINARY_OP_ADD_INT 307 +#define _BINARY_OP_ADD_INT_INPLACE 308 +#define _BINARY_OP_ADD_INT_INPLACE_RIGHT 309 +#define _BINARY_OP_ADD_UNICODE 310 +#define _BINARY_OP_EXTEND 311 +#define _BINARY_OP_INPLACE_ADD_UNICODE 312 +#define _BINARY_OP_MULTIPLY_FLOAT 313 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE 314 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT 315 +#define _BINARY_OP_MULTIPLY_INT 316 +#define _BINARY_OP_MULTIPLY_INT_INPLACE 317 +#define _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT 318 +#define _BINARY_OP_SUBSCR_CHECK_FUNC 319 +#define _BINARY_OP_SUBSCR_DICT 320 +#define _BINARY_OP_SUBSCR_DICT_KNOWN_HASH 321 +#define _BINARY_OP_SUBSCR_INIT_CALL 322 +#define _BINARY_OP_SUBSCR_LIST_INT 323 +#define _BINARY_OP_SUBSCR_LIST_SLICE 324 +#define _BINARY_OP_SUBSCR_STR_INT 325 +#define _BINARY_OP_SUBSCR_TUPLE_INT 326 +#define _BINARY_OP_SUBSCR_USTR_INT 327 +#define _BINARY_OP_SUBTRACT_FLOAT 328 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE 329 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT 330 +#define _BINARY_OP_SUBTRACT_INT 331 +#define _BINARY_OP_SUBTRACT_INT_INPLACE 332 +#define _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT 333 +#define _BINARY_OP_TRUEDIV_FLOAT 334 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE 335 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT 336 +#define _BINARY_SLICE 337 #define _BUILD_INTERPOLATION BUILD_INTERPOLATION #define _BUILD_LIST BUILD_LIST #define _BUILD_MAP BUILD_MAP @@ -37,131 +55,199 @@ extern "C" { #define _BUILD_STRING BUILD_STRING #define _BUILD_TEMPLATE BUILD_TEMPLATE #define _BUILD_TUPLE BUILD_TUPLE -#define _CALL_BUILTIN_CLASS 320 -#define _CALL_BUILTIN_FAST 321 -#define _CALL_BUILTIN_FAST_WITH_KEYWORDS 322 -#define _CALL_BUILTIN_O 323 -#define _CALL_INTRINSIC_1 CALL_INTRINSIC_1 -#define _CALL_INTRINSIC_2 CALL_INTRINSIC_2 -#define _CALL_ISINSTANCE CALL_ISINSTANCE -#define _CALL_KW_NON_PY 324 -#define _CALL_LEN 325 -#define _CALL_LIST_APPEND CALL_LIST_APPEND -#define _CALL_METHOD_DESCRIPTOR_FAST 326 -#define _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS 327 -#define _CALL_METHOD_DESCRIPTOR_NOARGS 328 -#define _CALL_METHOD_DESCRIPTOR_O 329 -#define _CALL_NON_PY_GENERAL 330 -#define _CALL_STR_1 331 -#define _CALL_TUPLE_1 332 -#define _CALL_TYPE_1 333 -#define _CHECK_AND_ALLOCATE_OBJECT 334 -#define _CHECK_ATTR_CLASS 335 -#define _CHECK_ATTR_METHOD_LAZY_DICT 336 -#define _CHECK_CALL_BOUND_METHOD_EXACT_ARGS 337 +#define _CALL_BUILTIN_CLASS 338 +#define _CALL_BUILTIN_FAST 339 +#define _CALL_BUILTIN_FAST_WITH_KEYWORDS 340 +#define _CALL_BUILTIN_O 341 +#define _CALL_FUNCTION_EX_NON_PY_GENERAL 342 +#define _CALL_INTRINSIC_1 343 +#define _CALL_INTRINSIC_2 344 +#define _CALL_ISINSTANCE 345 +#define _CALL_KW_NON_PY 346 +#define _CALL_LEN 347 +#define _CALL_LIST_APPEND 348 +#define _CALL_METHOD_DESCRIPTOR_FAST 349 +#define _CALL_METHOD_DESCRIPTOR_FAST_INLINE 350 +#define _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS 351 +#define _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE 352 +#define _CALL_METHOD_DESCRIPTOR_NOARGS 353 +#define _CALL_METHOD_DESCRIPTOR_NOARGS_INLINE 354 +#define _CALL_METHOD_DESCRIPTOR_O 355 +#define _CALL_METHOD_DESCRIPTOR_O_INLINE 356 +#define _CALL_NON_PY_GENERAL 357 +#define _CALL_STR_1 358 +#define _CALL_TUPLE_1 359 +#define _CALL_TYPE_1 360 +#define _CHECK_ATTR_CLASS 361 +#define _CHECK_ATTR_METHOD_LAZY_DICT 362 +#define _CHECK_CALL_BOUND_METHOD_EXACT_ARGS 363 #define _CHECK_EG_MATCH CHECK_EG_MATCH #define _CHECK_EXC_MATCH CHECK_EXC_MATCH -#define _CHECK_FUNCTION 338 -#define _CHECK_FUNCTION_EXACT_ARGS 339 -#define _CHECK_FUNCTION_VERSION 340 -#define _CHECK_FUNCTION_VERSION_INLINE 341 -#define _CHECK_FUNCTION_VERSION_KW 342 -#define _CHECK_IS_NOT_PY_CALLABLE 343 -#define _CHECK_IS_NOT_PY_CALLABLE_KW 344 -#define _CHECK_MANAGED_OBJECT_HAS_VALUES 345 -#define _CHECK_METHOD_VERSION 346 -#define _CHECK_METHOD_VERSION_KW 347 -#define _CHECK_PEP_523 348 -#define _CHECK_PERIODIC 349 -#define _CHECK_PERIODIC_IF_NOT_YIELD_FROM 350 -#define _CHECK_RECURSION_REMAINING 351 -#define _CHECK_STACK_SPACE 352 -#define _CHECK_STACK_SPACE_OPERAND 353 -#define _CHECK_VALIDITY 354 -#define _COMPARE_OP 355 -#define _COMPARE_OP_FLOAT 356 -#define _COMPARE_OP_INT 357 -#define _COMPARE_OP_STR 358 -#define _CONTAINS_OP 359 -#define _CONTAINS_OP_DICT 360 -#define _CONTAINS_OP_SET 361 +#define _CHECK_FUNCTION_EXACT_ARGS 364 +#define _CHECK_FUNCTION_VERSION 365 +#define _CHECK_FUNCTION_VERSION_INLINE 366 +#define _CHECK_FUNCTION_VERSION_KW 367 +#define _CHECK_IS_NOT_PY_CALLABLE 368 +#define _CHECK_IS_NOT_PY_CALLABLE_EX 369 +#define _CHECK_IS_NOT_PY_CALLABLE_KW 370 +#define _CHECK_IS_PY_CALLABLE_EX 371 +#define _CHECK_MANAGED_OBJECT_HAS_VALUES 372 +#define _CHECK_METHOD_VERSION 373 +#define _CHECK_METHOD_VERSION_KW 374 +#define _CHECK_OBJECT 375 +#define _CHECK_PEP_523 376 +#define _CHECK_PERIODIC 377 +#define _CHECK_PERIODIC_AT_END 378 +#define _CHECK_PERIODIC_IF_NOT_YIELD_FROM 379 +#define _CHECK_RECURSION_LIMIT 380 +#define _CHECK_RECURSION_REMAINING 381 +#define _CHECK_STACK_SPACE 382 +#define _CHECK_STACK_SPACE_OPERAND 383 +#define _CHECK_VALIDITY 384 +#define _COLD_DYNAMIC_EXIT 385 +#define _COLD_EXIT 386 +#define _COMPARE_OP 387 +#define _COMPARE_OP_FLOAT 388 +#define _COMPARE_OP_INT 389 +#define _COMPARE_OP_STR 390 +#define _CONTAINS_OP 391 +#define _CONTAINS_OP_DICT 392 +#define _CONTAINS_OP_SET 393 #define _CONVERT_VALUE CONVERT_VALUE -#define _COPY COPY +#define _COPY 394 +#define _COPY_1 395 +#define _COPY_2 396 +#define _COPY_3 397 #define _COPY_FREE_VARS COPY_FREE_VARS -#define _CREATE_INIT_FRAME 362 +#define _CREATE_INIT_FRAME 398 #define _DELETE_ATTR DELETE_ATTR #define _DELETE_DEREF DELETE_DEREF #define _DELETE_FAST DELETE_FAST #define _DELETE_GLOBAL DELETE_GLOBAL #define _DELETE_NAME DELETE_NAME #define _DELETE_SUBSCR DELETE_SUBSCR -#define _DEOPT 363 -#define _DICT_MERGE DICT_MERGE -#define _DICT_UPDATE DICT_UPDATE -#define _DO_CALL 364 -#define _DO_CALL_FUNCTION_EX 365 -#define _DO_CALL_KW 366 +#define _DEOPT 399 +#define _DICT_MERGE 400 +#define _DICT_UPDATE 401 +#define _DO_CALL 402 +#define _DO_CALL_FUNCTION_EX 403 +#define _DO_CALL_KW 404 +#define _DYNAMIC_EXIT 405 #define _END_FOR END_FOR #define _END_SEND END_SEND -#define _ERROR_POP_N 367 +#define _ERROR_POP_N 406 #define _EXIT_INIT_CHECK EXIT_INIT_CHECK -#define _EXPAND_METHOD 368 -#define _EXPAND_METHOD_KW 369 -#define _FATAL_ERROR 370 +#define _EXPAND_METHOD 407 +#define _EXPAND_METHOD_KW 408 +#define _FATAL_ERROR 409 #define _FORMAT_SIMPLE FORMAT_SIMPLE #define _FORMAT_WITH_SPEC FORMAT_WITH_SPEC -#define _FOR_ITER 371 -#define _FOR_ITER_GEN_FRAME 372 -#define _FOR_ITER_TIER_TWO 373 +#define _FOR_ITER 410 +#define _FOR_ITER_GEN_FRAME 411 +#define _FOR_ITER_TIER_TWO 412 +#define _FOR_ITER_VIRTUAL 413 +#define _FOR_ITER_VIRTUAL_TIER_TWO 414 #define _GET_AITER GET_AITER #define _GET_ANEXT GET_ANEXT #define _GET_AWAITABLE GET_AWAITABLE -#define _GET_ITER GET_ITER +#define _GET_ITER 415 +#define _GET_ITER_TRAD 416 #define _GET_LEN GET_LEN -#define _GET_YIELD_FROM_ITER GET_YIELD_FROM_ITER -#define _GUARD_BINARY_OP_EXTEND 374 -#define _GUARD_CALLABLE_LEN 375 -#define _GUARD_CALLABLE_STR_1 376 -#define _GUARD_CALLABLE_TUPLE_1 377 -#define _GUARD_CALLABLE_TYPE_1 378 -#define _GUARD_DORV_NO_DICT 379 -#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT 380 -#define _GUARD_GLOBALS_VERSION 381 -#define _GUARD_IS_FALSE_POP 382 -#define _GUARD_IS_NONE_POP 383 -#define _GUARD_IS_NOT_NONE_POP 384 -#define _GUARD_IS_TRUE_POP 385 -#define _GUARD_KEYS_VERSION 386 -#define _GUARD_NOS_DICT 387 -#define _GUARD_NOS_FLOAT 388 -#define _GUARD_NOS_INT 389 -#define _GUARD_NOS_LIST 390 -#define _GUARD_NOS_NULL 391 -#define _GUARD_NOS_TUPLE 392 -#define _GUARD_NOS_UNICODE 393 -#define _GUARD_NOT_EXHAUSTED_LIST 394 -#define _GUARD_NOT_EXHAUSTED_RANGE 395 -#define _GUARD_NOT_EXHAUSTED_TUPLE 396 -#define _GUARD_TOS_ANY_SET 397 -#define _GUARD_TOS_DICT 398 -#define _GUARD_TOS_FLOAT 399 -#define _GUARD_TOS_INT 400 -#define _GUARD_TOS_LIST 401 -#define _GUARD_TOS_SLICE 402 -#define _GUARD_TOS_TUPLE 403 -#define _GUARD_TOS_UNICODE 404 -#define _GUARD_TYPE_VERSION 405 -#define _GUARD_TYPE_VERSION_AND_LOCK 406 +#define _GUARD_3OS_ASYNC_GEN_ASEND 417 +#define _GUARD_BINARY_OP_EXTEND 418 +#define _GUARD_BINARY_OP_EXTEND_LHS 419 +#define _GUARD_BINARY_OP_EXTEND_RHS 420 +#define _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS 421 +#define _GUARD_BIT_IS_SET_POP 422 +#define _GUARD_BIT_IS_SET_POP_4 423 +#define _GUARD_BIT_IS_SET_POP_5 424 +#define _GUARD_BIT_IS_SET_POP_6 425 +#define _GUARD_BIT_IS_SET_POP_7 426 +#define _GUARD_BIT_IS_UNSET_POP 427 +#define _GUARD_BIT_IS_UNSET_POP_4 428 +#define _GUARD_BIT_IS_UNSET_POP_5 429 +#define _GUARD_BIT_IS_UNSET_POP_6 430 +#define _GUARD_BIT_IS_UNSET_POP_7 431 +#define _GUARD_CALLABLE_BUILTIN_CLASS 432 +#define _GUARD_CALLABLE_BUILTIN_FAST 433 +#define _GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS 434 +#define _GUARD_CALLABLE_BUILTIN_O 435 +#define _GUARD_CALLABLE_ISINSTANCE 436 +#define _GUARD_CALLABLE_LEN 437 +#define _GUARD_CALLABLE_LIST_APPEND 438 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST 439 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS 440 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS 441 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_O 442 +#define _GUARD_CALLABLE_STR_1 443 +#define _GUARD_CALLABLE_TUPLE_1 444 +#define _GUARD_CALLABLE_TYPE_1 445 +#define _GUARD_CODE_VERSION_RETURN_GENERATOR 446 +#define _GUARD_CODE_VERSION_RETURN_VALUE 447 +#define _GUARD_CODE_VERSION_YIELD_VALUE 448 +#define _GUARD_CODE_VERSION__PUSH_FRAME 449 +#define _GUARD_DORV_NO_DICT 450 +#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT 451 +#define _GUARD_GLOBALS_VERSION 452 +#define _GUARD_IP_RETURN_GENERATOR 453 +#define _GUARD_IP_RETURN_VALUE 454 +#define _GUARD_IP_YIELD_VALUE 455 +#define _GUARD_IP__PUSH_FRAME 456 +#define _GUARD_IS_FALSE_POP 457 +#define _GUARD_IS_NONE_POP 458 +#define _GUARD_IS_NOT_NONE_POP 459 +#define _GUARD_IS_TRUE_POP 460 +#define _GUARD_ITERATOR 461 +#define _GUARD_ITER_VIRTUAL 462 +#define _GUARD_KEYS_VERSION 463 +#define _GUARD_LOAD_SUPER_ATTR_METHOD 464 +#define _GUARD_NOS_COMPACT_ASCII 465 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT 466 +#define _GUARD_NOS_DICT_SUBSCRIPT 467 +#define _GUARD_NOS_FLOAT 468 +#define _GUARD_NOS_INT 469 +#define _GUARD_NOS_ITER_VIRTUAL 470 +#define _GUARD_NOS_LIST 471 +#define _GUARD_NOS_NOT_NULL 472 +#define _GUARD_NOS_NULL 473 +#define _GUARD_NOS_OVERFLOWED 474 +#define _GUARD_NOS_TUPLE 475 +#define _GUARD_NOS_TYPE_VERSION 476 +#define _GUARD_NOS_UNICODE 477 +#define _GUARD_NOT_EXHAUSTED_LIST 478 +#define _GUARD_NOT_EXHAUSTED_RANGE 479 +#define _GUARD_NOT_EXHAUSTED_TUPLE 480 +#define _GUARD_THIRD_NULL 481 +#define _GUARD_TOS_ANY_DICT 482 +#define _GUARD_TOS_ANY_SET 483 +#define _GUARD_TOS_DICT 484 +#define _GUARD_TOS_FLOAT 485 +#define _GUARD_TOS_FROZENDICT 486 +#define _GUARD_TOS_FROZENSET 487 +#define _GUARD_TOS_INT 488 +#define _GUARD_TOS_IS_NONE 489 +#define _GUARD_TOS_LIST 490 +#define _GUARD_TOS_NOT_NULL 491 +#define _GUARD_TOS_OVERFLOWED 492 +#define _GUARD_TOS_SET 493 +#define _GUARD_TOS_SLICE 494 +#define _GUARD_TOS_TUPLE 495 +#define _GUARD_TOS_UNICODE 496 +#define _GUARD_TYPE 497 +#define _GUARD_TYPE_ITER 498 +#define _GUARD_TYPE_VERSION 499 +#define _GUARD_TYPE_VERSION_LOCKED 500 +#define _HANDLE_PENDING_AND_DEOPT 501 #define _IMPORT_FROM IMPORT_FROM #define _IMPORT_NAME IMPORT_NAME -#define _INIT_CALL_BOUND_METHOD_EXACT_ARGS 407 -#define _INIT_CALL_PY_EXACT_ARGS 408 -#define _INIT_CALL_PY_EXACT_ARGS_0 409 -#define _INIT_CALL_PY_EXACT_ARGS_1 410 -#define _INIT_CALL_PY_EXACT_ARGS_2 411 -#define _INIT_CALL_PY_EXACT_ARGS_3 412 -#define _INIT_CALL_PY_EXACT_ARGS_4 413 -#define _INSERT_NULL 414 +#define _INIT_CALL_BOUND_METHOD_EXACT_ARGS 502 +#define _INIT_CALL_PY_EXACT_ARGS 503 +#define _INIT_CALL_PY_EXACT_ARGS_0 504 +#define _INIT_CALL_PY_EXACT_ARGS_1 505 +#define _INIT_CALL_PY_EXACT_ARGS_2 506 +#define _INIT_CALL_PY_EXACT_ARGS_3 507 +#define _INIT_CALL_PY_EXACT_ARGS_4 508 +#define _INSERT_NULL 509 #define _INSTRUMENTED_FOR_ITER INSTRUMENTED_FOR_ITER #define _INSTRUMENTED_INSTRUCTION INSTRUMENTED_INSTRUCTION #define _INSTRUMENTED_JUMP_FORWARD INSTRUMENTED_JUMP_FORWARD @@ -171,163 +257,1194 @@ extern "C" { #define _INSTRUMENTED_POP_JUMP_IF_NONE INSTRUMENTED_POP_JUMP_IF_NONE #define _INSTRUMENTED_POP_JUMP_IF_NOT_NONE INSTRUMENTED_POP_JUMP_IF_NOT_NONE #define _INSTRUMENTED_POP_JUMP_IF_TRUE INSTRUMENTED_POP_JUMP_IF_TRUE -#define _IS_NONE 415 -#define _IS_OP IS_OP -#define _ITER_CHECK_LIST 416 -#define _ITER_CHECK_RANGE 417 -#define _ITER_CHECK_TUPLE 418 -#define _ITER_JUMP_LIST 419 -#define _ITER_JUMP_RANGE 420 -#define _ITER_JUMP_TUPLE 421 -#define _ITER_NEXT_LIST 422 -#define _ITER_NEXT_LIST_TIER_TWO 423 -#define _ITER_NEXT_RANGE 424 -#define _ITER_NEXT_TUPLE 425 -#define _JUMP_TO_TOP 426 +#define _IS_NONE 510 +#define _IS_OP 511 +#define _ITER_CHECK_LIST 512 +#define _ITER_CHECK_RANGE 513 +#define _ITER_CHECK_TUPLE 514 +#define _ITER_JUMP_LIST 515 +#define _ITER_JUMP_RANGE 516 +#define _ITER_JUMP_TUPLE 517 +#define _ITER_NEXT_INLINE 518 +#define _ITER_NEXT_LIST 519 +#define _ITER_NEXT_LIST_TIER_TWO 520 +#define _ITER_NEXT_RANGE 521 +#define _ITER_NEXT_TUPLE 522 +#define _JUMP_BACKWARD_NO_INTERRUPT JUMP_BACKWARD_NO_INTERRUPT +#define _JUMP_TO_TOP 523 #define _LIST_APPEND LIST_APPEND -#define _LIST_EXTEND LIST_EXTEND -#define _LOAD_ATTR 427 -#define _LOAD_ATTR_CLASS 428 -#define _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN -#define _LOAD_ATTR_INSTANCE_VALUE 429 -#define _LOAD_ATTR_METHOD_LAZY_DICT 430 -#define _LOAD_ATTR_METHOD_NO_DICT 431 -#define _LOAD_ATTR_METHOD_WITH_VALUES 432 -#define _LOAD_ATTR_MODULE 433 -#define _LOAD_ATTR_NONDESCRIPTOR_NO_DICT 434 -#define _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES 435 -#define _LOAD_ATTR_PROPERTY_FRAME 436 -#define _LOAD_ATTR_SLOT 437 -#define _LOAD_ATTR_WITH_HINT 438 +#define _LIST_EXTEND 524 +#define _LOAD_ATTR 525 +#define _LOAD_ATTR_CLASS 526 +#define _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME 527 +#define _LOAD_ATTR_INSTANCE_VALUE 528 +#define _LOAD_ATTR_METHOD_LAZY_DICT 529 +#define _LOAD_ATTR_METHOD_NO_DICT 530 +#define _LOAD_ATTR_METHOD_WITH_VALUES 531 +#define _LOAD_ATTR_MODULE 532 +#define _LOAD_ATTR_NONDESCRIPTOR_NO_DICT 533 +#define _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES 534 +#define _LOAD_ATTR_PROPERTY_FRAME 535 +#define _LOAD_ATTR_SLOT 536 +#define _LOAD_ATTR_WITH_HINT 537 #define _LOAD_BUILD_CLASS LOAD_BUILD_CLASS -#define _LOAD_BYTECODE 439 +#define _LOAD_BYTECODE 538 #define _LOAD_COMMON_CONSTANT LOAD_COMMON_CONSTANT #define _LOAD_CONST LOAD_CONST -#define _LOAD_CONST_IMMORTAL LOAD_CONST_IMMORTAL -#define _LOAD_CONST_INLINE 440 -#define _LOAD_CONST_INLINE_BORROW 441 -#define _LOAD_CONST_MORTAL LOAD_CONST_MORTAL +#define _LOAD_CONST_INLINE 539 +#define _LOAD_CONST_INLINE_BORROW 540 #define _LOAD_DEREF LOAD_DEREF -#define _LOAD_FAST 442 -#define _LOAD_FAST_0 443 -#define _LOAD_FAST_1 444 -#define _LOAD_FAST_2 445 -#define _LOAD_FAST_3 446 -#define _LOAD_FAST_4 447 -#define _LOAD_FAST_5 448 -#define _LOAD_FAST_6 449 -#define _LOAD_FAST_7 450 +#define _LOAD_FAST 541 +#define _LOAD_FAST_0 542 +#define _LOAD_FAST_1 543 +#define _LOAD_FAST_2 544 +#define _LOAD_FAST_3 545 +#define _LOAD_FAST_4 546 +#define _LOAD_FAST_5 547 +#define _LOAD_FAST_6 548 +#define _LOAD_FAST_7 549 #define _LOAD_FAST_AND_CLEAR LOAD_FAST_AND_CLEAR -#define _LOAD_FAST_BORROW 451 -#define _LOAD_FAST_BORROW_0 452 -#define _LOAD_FAST_BORROW_1 453 -#define _LOAD_FAST_BORROW_2 454 -#define _LOAD_FAST_BORROW_3 455 -#define _LOAD_FAST_BORROW_4 456 -#define _LOAD_FAST_BORROW_5 457 -#define _LOAD_FAST_BORROW_6 458 -#define _LOAD_FAST_BORROW_7 459 -#define _LOAD_FAST_BORROW_LOAD_FAST_BORROW LOAD_FAST_BORROW_LOAD_FAST_BORROW +#define _LOAD_FAST_BORROW 550 +#define _LOAD_FAST_BORROW_0 551 +#define _LOAD_FAST_BORROW_1 552 +#define _LOAD_FAST_BORROW_2 553 +#define _LOAD_FAST_BORROW_3 554 +#define _LOAD_FAST_BORROW_4 555 +#define _LOAD_FAST_BORROW_5 556 +#define _LOAD_FAST_BORROW_6 557 +#define _LOAD_FAST_BORROW_7 558 #define _LOAD_FAST_CHECK LOAD_FAST_CHECK -#define _LOAD_FAST_LOAD_FAST LOAD_FAST_LOAD_FAST #define _LOAD_FROM_DICT_OR_DEREF LOAD_FROM_DICT_OR_DEREF #define _LOAD_FROM_DICT_OR_GLOBALS LOAD_FROM_DICT_OR_GLOBALS -#define _LOAD_GLOBAL 460 -#define _LOAD_GLOBAL_BUILTINS 461 -#define _LOAD_GLOBAL_MODULE 462 +#define _LOAD_GLOBAL 559 +#define _LOAD_GLOBAL_BUILTINS 560 +#define _LOAD_GLOBAL_MODULE 561 #define _LOAD_LOCALS LOAD_LOCALS #define _LOAD_NAME LOAD_NAME -#define _LOAD_SMALL_INT 463 -#define _LOAD_SMALL_INT_0 464 -#define _LOAD_SMALL_INT_1 465 -#define _LOAD_SMALL_INT_2 466 -#define _LOAD_SMALL_INT_3 467 -#define _LOAD_SPECIAL 468 +#define _LOAD_SMALL_INT 562 +#define _LOAD_SMALL_INT_0 563 +#define _LOAD_SMALL_INT_1 564 +#define _LOAD_SMALL_INT_2 565 +#define _LOAD_SMALL_INT_3 566 +#define _LOAD_SPECIAL 567 #define _LOAD_SUPER_ATTR_ATTR LOAD_SUPER_ATTR_ATTR -#define _LOAD_SUPER_ATTR_METHOD LOAD_SUPER_ATTR_METHOD -#define _MAKE_CALLARGS_A_TUPLE 469 +#define _LOAD_SUPER_ATTR_METHOD 568 +#define _LOCK_OBJECT 569 +#define _MAKE_CALLARGS_A_TUPLE 570 #define _MAKE_CELL MAKE_CELL -#define _MAKE_FUNCTION MAKE_FUNCTION -#define _MAKE_WARM 470 +#define _MAKE_FUNCTION 571 +#define _MAKE_HEAP_SAFE 572 +#define _MAKE_WARM 573 #define _MAP_ADD MAP_ADD -#define _MATCH_CLASS MATCH_CLASS +#define _MATCH_CLASS 574 #define _MATCH_KEYS MATCH_KEYS #define _MATCH_MAPPING MATCH_MAPPING #define _MATCH_SEQUENCE MATCH_SEQUENCE -#define _MAYBE_EXPAND_METHOD 471 -#define _MAYBE_EXPAND_METHOD_KW 472 -#define _MONITOR_CALL 473 -#define _MONITOR_CALL_KW 474 -#define _MONITOR_JUMP_BACKWARD 475 -#define _MONITOR_RESUME 476 +#define _MAYBE_EXPAND_METHOD 575 +#define _MAYBE_EXPAND_METHOD_KW 576 +#define _MONITOR_CALL 577 +#define _MONITOR_CALL_KW 578 +#define _MONITOR_JUMP_BACKWARD 579 +#define _MONITOR_RESUME 580 #define _NOP NOP #define _POP_EXCEPT POP_EXCEPT -#define _POP_JUMP_IF_FALSE 477 -#define _POP_JUMP_IF_TRUE 478 +#define _POP_ITER POP_ITER +#define _POP_JUMP_IF_FALSE 581 +#define _POP_JUMP_IF_TRUE 582 #define _POP_TOP POP_TOP -#define _POP_TOP_LOAD_CONST_INLINE 479 -#define _POP_TOP_LOAD_CONST_INLINE_BORROW 480 -#define _POP_TWO_LOAD_CONST_INLINE_BORROW 481 +#define _POP_TOP_FLOAT 583 +#define _POP_TOP_INT 584 +#define _POP_TOP_NOP 585 +#define _POP_TOP_OPARG 586 +#define _POP_TOP_UNICODE 587 #define _PUSH_EXC_INFO PUSH_EXC_INFO -#define _PUSH_FRAME 482 +#define _PUSH_FRAME 588 #define _PUSH_NULL PUSH_NULL -#define _PUSH_NULL_CONDITIONAL 483 -#define _PY_FRAME_GENERAL 484 -#define _PY_FRAME_KW 485 -#define _QUICKEN_RESUME 486 -#define _REPLACE_WITH_TRUE 487 -#define _RESUME_CHECK RESUME_CHECK +#define _PUSH_NULL_CONDITIONAL 589 +#define _PUSH_TAGGED_ZERO 590 +#define _PY_FRAME_EX 591 +#define _PY_FRAME_GENERAL 592 +#define _PY_FRAME_KW 593 +#define _RECORD_3OS_GEN_FUNC 594 +#define _RECORD_4OS 595 +#define _RECORD_BOUND_METHOD 596 +#define _RECORD_CALLABLE 597 +#define _RECORD_CALLABLE_KW 598 +#define _RECORD_CODE 599 +#define _RECORD_NOS 600 +#define _RECORD_NOS_GEN_FUNC 601 +#define _RECORD_NOS_TYPE 602 +#define _RECORD_TOS 603 +#define _RECORD_TOS_TYPE 604 +#define _REPLACE_WITH_TRUE 605 +#define _RESUME_CHECK 606 #define _RETURN_GENERATOR RETURN_GENERATOR -#define _RETURN_VALUE RETURN_VALUE -#define _SAVE_RETURN_OFFSET 488 -#define _SEND 489 -#define _SEND_GEN_FRAME 490 +#define _RETURN_VALUE 607 +#define _RROT_3 608 +#define _SAVE_RETURN_OFFSET 609 +#define _SEND_ASYNC_GEN 610 +#define _SEND_ASYNC_GEN_TIER_TWO 611 +#define _SEND_GEN_FRAME 612 +#define _SEND_VIRTUAL 613 +#define _SEND_VIRTUAL_TIER_TWO 614 #define _SETUP_ANNOTATIONS SETUP_ANNOTATIONS #define _SET_ADD SET_ADD #define _SET_FUNCTION_ATTRIBUTE SET_FUNCTION_ATTRIBUTE -#define _SET_UPDATE SET_UPDATE -#define _START_EXECUTOR 491 -#define _STORE_ATTR 492 -#define _STORE_ATTR_INSTANCE_VALUE 493 -#define _STORE_ATTR_SLOT 494 -#define _STORE_ATTR_WITH_HINT 495 +#define _SET_UPDATE 615 +#define _SPILL_OR_RELOAD 616 +#define _START_EXECUTOR 617 +#define _STORE_ATTR 618 +#define _STORE_ATTR_INSTANCE_VALUE 619 +#define _STORE_ATTR_SLOT 620 +#define _STORE_ATTR_WITH_HINT 621 #define _STORE_DEREF STORE_DEREF -#define _STORE_FAST 496 -#define _STORE_FAST_0 497 -#define _STORE_FAST_1 498 -#define _STORE_FAST_2 499 -#define _STORE_FAST_3 500 -#define _STORE_FAST_4 501 -#define _STORE_FAST_5 502 -#define _STORE_FAST_6 503 -#define _STORE_FAST_7 504 -#define _STORE_FAST_LOAD_FAST STORE_FAST_LOAD_FAST -#define _STORE_FAST_STORE_FAST STORE_FAST_STORE_FAST #define _STORE_GLOBAL STORE_GLOBAL #define _STORE_NAME STORE_NAME -#define _STORE_SLICE 505 -#define _STORE_SUBSCR 506 -#define _STORE_SUBSCR_DICT 507 -#define _STORE_SUBSCR_LIST_INT 508 -#define _SWAP SWAP -#define _TIER2_RESUME_CHECK 509 -#define _TO_BOOL 510 +#define _STORE_SLICE 622 +#define _STORE_SUBSCR 623 +#define _STORE_SUBSCR_DICT 624 +#define _STORE_SUBSCR_DICT_KNOWN_HASH 625 +#define _STORE_SUBSCR_LIST_INT 626 +#define _SWAP 627 +#define _SWAP_2 628 +#define _SWAP_3 629 +#define _SWAP_FAST 630 +#define _SWAP_FAST_0 631 +#define _SWAP_FAST_1 632 +#define _SWAP_FAST_2 633 +#define _SWAP_FAST_3 634 +#define _SWAP_FAST_4 635 +#define _SWAP_FAST_5 636 +#define _SWAP_FAST_6 637 +#define _SWAP_FAST_7 638 +#define _TIER2_RESUME_CHECK 639 +#define _TO_BOOL 640 #define _TO_BOOL_BOOL TO_BOOL_BOOL -#define _TO_BOOL_INT TO_BOOL_INT -#define _TO_BOOL_LIST 511 +#define _TO_BOOL_INT 641 +#define _TO_BOOL_LIST 642 #define _TO_BOOL_NONE TO_BOOL_NONE -#define _TO_BOOL_STR 512 -#define _UNARY_INVERT UNARY_INVERT -#define _UNARY_NEGATIVE UNARY_NEGATIVE +#define _TO_BOOL_STR 643 +#define _TRACE_RECORD TRACE_RECORD +#define _UNARY_INVERT 644 +#define _UNARY_NEGATIVE 645 +#define _UNARY_NEGATIVE_FLOAT_INPLACE 646 #define _UNARY_NOT UNARY_NOT #define _UNPACK_EX UNPACK_EX -#define _UNPACK_SEQUENCE 513 -#define _UNPACK_SEQUENCE_LIST 514 -#define _UNPACK_SEQUENCE_TUPLE 515 -#define _UNPACK_SEQUENCE_TWO_TUPLE 516 +#define _UNPACK_SEQUENCE 647 +#define _UNPACK_SEQUENCE_LIST 648 +#define _UNPACK_SEQUENCE_TUPLE 649 +#define _UNPACK_SEQUENCE_TWO_TUPLE 650 +#define _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE 651 +#define _UNPACK_SEQUENCE_UNIQUE_TUPLE 652 +#define _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE 653 #define _WITH_EXCEPT_START WITH_EXCEPT_START -#define _YIELD_VALUE YIELD_VALUE -#define MAX_UOP_ID 516 +#define _YIELD_VALUE 654 +#define MAX_UOP_ID 654 +#define _ALLOCATE_OBJECT_r00 655 +#define _BINARY_OP_r23 656 +#define _BINARY_OP_ADD_FLOAT_r03 657 +#define _BINARY_OP_ADD_FLOAT_r13 658 +#define _BINARY_OP_ADD_FLOAT_r23 659 +#define _BINARY_OP_ADD_FLOAT_INPLACE_r03 660 +#define _BINARY_OP_ADD_FLOAT_INPLACE_r13 661 +#define _BINARY_OP_ADD_FLOAT_INPLACE_r23 662 +#define _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r03 663 +#define _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r13 664 +#define _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r23 665 +#define _BINARY_OP_ADD_INT_r03 666 +#define _BINARY_OP_ADD_INT_r13 667 +#define _BINARY_OP_ADD_INT_r23 668 +#define _BINARY_OP_ADD_INT_INPLACE_r03 669 +#define _BINARY_OP_ADD_INT_INPLACE_r13 670 +#define _BINARY_OP_ADD_INT_INPLACE_r23 671 +#define _BINARY_OP_ADD_INT_INPLACE_RIGHT_r03 672 +#define _BINARY_OP_ADD_INT_INPLACE_RIGHT_r13 673 +#define _BINARY_OP_ADD_INT_INPLACE_RIGHT_r23 674 +#define _BINARY_OP_ADD_UNICODE_r03 675 +#define _BINARY_OP_ADD_UNICODE_r13 676 +#define _BINARY_OP_ADD_UNICODE_r23 677 +#define _BINARY_OP_EXTEND_r23 678 +#define _BINARY_OP_INPLACE_ADD_UNICODE_r21 679 +#define _BINARY_OP_MULTIPLY_FLOAT_r03 680 +#define _BINARY_OP_MULTIPLY_FLOAT_r13 681 +#define _BINARY_OP_MULTIPLY_FLOAT_r23 682 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE_r03 683 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE_r13 684 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE_r23 685 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r03 686 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r13 687 +#define _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r23 688 +#define _BINARY_OP_MULTIPLY_INT_r03 689 +#define _BINARY_OP_MULTIPLY_INT_r13 690 +#define _BINARY_OP_MULTIPLY_INT_r23 691 +#define _BINARY_OP_MULTIPLY_INT_INPLACE_r03 692 +#define _BINARY_OP_MULTIPLY_INT_INPLACE_r13 693 +#define _BINARY_OP_MULTIPLY_INT_INPLACE_r23 694 +#define _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r03 695 +#define _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r13 696 +#define _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r23 697 +#define _BINARY_OP_SUBSCR_CHECK_FUNC_r23 698 +#define _BINARY_OP_SUBSCR_DICT_r23 699 +#define _BINARY_OP_SUBSCR_DICT_KNOWN_HASH_r23 700 +#define _BINARY_OP_SUBSCR_INIT_CALL_r01 701 +#define _BINARY_OP_SUBSCR_INIT_CALL_r11 702 +#define _BINARY_OP_SUBSCR_INIT_CALL_r21 703 +#define _BINARY_OP_SUBSCR_INIT_CALL_r31 704 +#define _BINARY_OP_SUBSCR_LIST_INT_r23 705 +#define _BINARY_OP_SUBSCR_LIST_SLICE_r23 706 +#define _BINARY_OP_SUBSCR_STR_INT_r23 707 +#define _BINARY_OP_SUBSCR_TUPLE_INT_r03 708 +#define _BINARY_OP_SUBSCR_TUPLE_INT_r13 709 +#define _BINARY_OP_SUBSCR_TUPLE_INT_r23 710 +#define _BINARY_OP_SUBSCR_USTR_INT_r23 711 +#define _BINARY_OP_SUBTRACT_FLOAT_r03 712 +#define _BINARY_OP_SUBTRACT_FLOAT_r13 713 +#define _BINARY_OP_SUBTRACT_FLOAT_r23 714 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE_r03 715 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE_r13 716 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE_r23 717 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r03 718 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r13 719 +#define _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r23 720 +#define _BINARY_OP_SUBTRACT_INT_r03 721 +#define _BINARY_OP_SUBTRACT_INT_r13 722 +#define _BINARY_OP_SUBTRACT_INT_r23 723 +#define _BINARY_OP_SUBTRACT_INT_INPLACE_r03 724 +#define _BINARY_OP_SUBTRACT_INT_INPLACE_r13 725 +#define _BINARY_OP_SUBTRACT_INT_INPLACE_r23 726 +#define _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r03 727 +#define _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r13 728 +#define _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r23 729 +#define _BINARY_OP_TRUEDIV_FLOAT_r23 730 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE_r03 731 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE_r13 732 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE_r23 733 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r03 734 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r13 735 +#define _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r23 736 +#define _BINARY_SLICE_r31 737 +#define _BUILD_INTERPOLATION_r01 738 +#define _BUILD_LIST_r01 739 +#define _BUILD_MAP_r01 740 +#define _BUILD_SET_r01 741 +#define _BUILD_SLICE_r01 742 +#define _BUILD_STRING_r01 743 +#define _BUILD_TEMPLATE_r21 744 +#define _BUILD_TUPLE_r01 745 +#define _CALL_BUILTIN_CLASS_r00 746 +#define _CALL_BUILTIN_FAST_r00 747 +#define _CALL_BUILTIN_FAST_WITH_KEYWORDS_r00 748 +#define _CALL_BUILTIN_O_r03 749 +#define _CALL_FUNCTION_EX_NON_PY_GENERAL_r31 750 +#define _CALL_INTRINSIC_1_r12 751 +#define _CALL_INTRINSIC_2_r23 752 +#define _CALL_ISINSTANCE_r31 753 +#define _CALL_KW_NON_PY_r11 754 +#define _CALL_LEN_r33 755 +#define _CALL_LIST_APPEND_r03 756 +#define _CALL_LIST_APPEND_r13 757 +#define _CALL_LIST_APPEND_r23 758 +#define _CALL_LIST_APPEND_r33 759 +#define _CALL_METHOD_DESCRIPTOR_FAST_r00 760 +#define _CALL_METHOD_DESCRIPTOR_FAST_INLINE_r00 761 +#define _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00 762 +#define _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE_r00 763 +#define _CALL_METHOD_DESCRIPTOR_NOARGS_r03 764 +#define _CALL_METHOD_DESCRIPTOR_NOARGS_INLINE_r03 765 +#define _CALL_METHOD_DESCRIPTOR_O_r03 766 +#define _CALL_METHOD_DESCRIPTOR_O_INLINE_r03 767 +#define _CALL_NON_PY_GENERAL_r01 768 +#define _CALL_STR_1_r32 769 +#define _CALL_TUPLE_1_r32 770 +#define _CALL_TYPE_1_r02 771 +#define _CALL_TYPE_1_r12 772 +#define _CALL_TYPE_1_r22 773 +#define _CALL_TYPE_1_r32 774 +#define _CHECK_ATTR_CLASS_r01 775 +#define _CHECK_ATTR_CLASS_r11 776 +#define _CHECK_ATTR_CLASS_r22 777 +#define _CHECK_ATTR_CLASS_r33 778 +#define _CHECK_ATTR_METHOD_LAZY_DICT_r01 779 +#define _CHECK_ATTR_METHOD_LAZY_DICT_r11 780 +#define _CHECK_ATTR_METHOD_LAZY_DICT_r22 781 +#define _CHECK_ATTR_METHOD_LAZY_DICT_r33 782 +#define _CHECK_CALL_BOUND_METHOD_EXACT_ARGS_r00 783 +#define _CHECK_EG_MATCH_r22 784 +#define _CHECK_EXC_MATCH_r22 785 +#define _CHECK_FUNCTION_EXACT_ARGS_r00 786 +#define _CHECK_FUNCTION_VERSION_r00 787 +#define _CHECK_FUNCTION_VERSION_INLINE_r00 788 +#define _CHECK_FUNCTION_VERSION_INLINE_r11 789 +#define _CHECK_FUNCTION_VERSION_INLINE_r22 790 +#define _CHECK_FUNCTION_VERSION_INLINE_r33 791 +#define _CHECK_FUNCTION_VERSION_KW_r11 792 +#define _CHECK_IS_NOT_PY_CALLABLE_r00 793 +#define _CHECK_IS_NOT_PY_CALLABLE_EX_r03 794 +#define _CHECK_IS_NOT_PY_CALLABLE_EX_r13 795 +#define _CHECK_IS_NOT_PY_CALLABLE_EX_r23 796 +#define _CHECK_IS_NOT_PY_CALLABLE_EX_r33 797 +#define _CHECK_IS_NOT_PY_CALLABLE_KW_r11 798 +#define _CHECK_IS_PY_CALLABLE_EX_r03 799 +#define _CHECK_IS_PY_CALLABLE_EX_r13 800 +#define _CHECK_IS_PY_CALLABLE_EX_r23 801 +#define _CHECK_IS_PY_CALLABLE_EX_r33 802 +#define _CHECK_MANAGED_OBJECT_HAS_VALUES_r01 803 +#define _CHECK_MANAGED_OBJECT_HAS_VALUES_r11 804 +#define _CHECK_MANAGED_OBJECT_HAS_VALUES_r22 805 +#define _CHECK_MANAGED_OBJECT_HAS_VALUES_r33 806 +#define _CHECK_METHOD_VERSION_r00 807 +#define _CHECK_METHOD_VERSION_KW_r11 808 +#define _CHECK_OBJECT_r00 809 +#define _CHECK_PEP_523_r00 810 +#define _CHECK_PEP_523_r11 811 +#define _CHECK_PEP_523_r22 812 +#define _CHECK_PEP_523_r33 813 +#define _CHECK_PERIODIC_r00 814 +#define _CHECK_PERIODIC_AT_END_r00 815 +#define _CHECK_PERIODIC_IF_NOT_YIELD_FROM_r00 816 +#define _CHECK_RECURSION_LIMIT_r00 817 +#define _CHECK_RECURSION_LIMIT_r11 818 +#define _CHECK_RECURSION_LIMIT_r22 819 +#define _CHECK_RECURSION_LIMIT_r33 820 +#define _CHECK_RECURSION_REMAINING_r00 821 +#define _CHECK_RECURSION_REMAINING_r11 822 +#define _CHECK_RECURSION_REMAINING_r22 823 +#define _CHECK_RECURSION_REMAINING_r33 824 +#define _CHECK_STACK_SPACE_r00 825 +#define _CHECK_STACK_SPACE_OPERAND_r00 826 +#define _CHECK_STACK_SPACE_OPERAND_r11 827 +#define _CHECK_STACK_SPACE_OPERAND_r22 828 +#define _CHECK_STACK_SPACE_OPERAND_r33 829 +#define _CHECK_VALIDITY_r00 830 +#define _CHECK_VALIDITY_r11 831 +#define _CHECK_VALIDITY_r22 832 +#define _CHECK_VALIDITY_r33 833 +#define _COLD_DYNAMIC_EXIT_r00 834 +#define _COLD_EXIT_r00 835 +#define _COMPARE_OP_r21 836 +#define _COMPARE_OP_FLOAT_r03 837 +#define _COMPARE_OP_FLOAT_r13 838 +#define _COMPARE_OP_FLOAT_r23 839 +#define _COMPARE_OP_INT_r23 840 +#define _COMPARE_OP_STR_r23 841 +#define _CONTAINS_OP_r23 842 +#define _CONTAINS_OP_DICT_r23 843 +#define _CONTAINS_OP_SET_r23 844 +#define _CONVERT_VALUE_r11 845 +#define _COPY_r01 846 +#define _COPY_1_r02 847 +#define _COPY_1_r12 848 +#define _COPY_1_r23 849 +#define _COPY_2_r03 850 +#define _COPY_2_r13 851 +#define _COPY_2_r23 852 +#define _COPY_3_r03 853 +#define _COPY_3_r13 854 +#define _COPY_3_r23 855 +#define _COPY_3_r33 856 +#define _COPY_FREE_VARS_r00 857 +#define _COPY_FREE_VARS_r11 858 +#define _COPY_FREE_VARS_r22 859 +#define _COPY_FREE_VARS_r33 860 +#define _CREATE_INIT_FRAME_r01 861 +#define _DELETE_ATTR_r10 862 +#define _DELETE_DEREF_r00 863 +#define _DELETE_FAST_r00 864 +#define _DELETE_GLOBAL_r00 865 +#define _DELETE_NAME_r00 866 +#define _DELETE_SUBSCR_r20 867 +#define _DEOPT_r00 868 +#define _DEOPT_r10 869 +#define _DEOPT_r20 870 +#define _DEOPT_r30 871 +#define _DICT_MERGE_r11 872 +#define _DICT_UPDATE_r11 873 +#define _DO_CALL_r01 874 +#define _DO_CALL_FUNCTION_EX_r31 875 +#define _DO_CALL_KW_r11 876 +#define _DYNAMIC_EXIT_r00 877 +#define _DYNAMIC_EXIT_r10 878 +#define _DYNAMIC_EXIT_r20 879 +#define _DYNAMIC_EXIT_r30 880 +#define _END_FOR_r10 881 +#define _END_SEND_r31 882 +#define _ERROR_POP_N_r00 883 +#define _EXIT_INIT_CHECK_r10 884 +#define _EXIT_TRACE_r00 885 +#define _EXIT_TRACE_r10 886 +#define _EXIT_TRACE_r20 887 +#define _EXIT_TRACE_r30 888 +#define _EXPAND_METHOD_r00 889 +#define _EXPAND_METHOD_KW_r11 890 +#define _FATAL_ERROR_r00 891 +#define _FATAL_ERROR_r11 892 +#define _FATAL_ERROR_r22 893 +#define _FATAL_ERROR_r33 894 +#define _FORMAT_SIMPLE_r11 895 +#define _FORMAT_WITH_SPEC_r21 896 +#define _FOR_ITER_r23 897 +#define _FOR_ITER_GEN_FRAME_r03 898 +#define _FOR_ITER_GEN_FRAME_r13 899 +#define _FOR_ITER_GEN_FRAME_r23 900 +#define _FOR_ITER_TIER_TWO_r23 901 +#define _FOR_ITER_VIRTUAL_r23 902 +#define _FOR_ITER_VIRTUAL_TIER_TWO_r23 903 +#define _GET_AITER_r11 904 +#define _GET_ANEXT_r12 905 +#define _GET_AWAITABLE_r11 906 +#define _GET_ITER_r12 907 +#define _GET_ITER_TRAD_r12 908 +#define _GET_LEN_r12 909 +#define _GUARD_3OS_ASYNC_GEN_ASEND_r03 910 +#define _GUARD_3OS_ASYNC_GEN_ASEND_r13 911 +#define _GUARD_3OS_ASYNC_GEN_ASEND_r23 912 +#define _GUARD_3OS_ASYNC_GEN_ASEND_r33 913 +#define _GUARD_BINARY_OP_EXTEND_r22 914 +#define _GUARD_BINARY_OP_EXTEND_LHS_r02 915 +#define _GUARD_BINARY_OP_EXTEND_LHS_r12 916 +#define _GUARD_BINARY_OP_EXTEND_LHS_r22 917 +#define _GUARD_BINARY_OP_EXTEND_LHS_r33 918 +#define _GUARD_BINARY_OP_EXTEND_RHS_r02 919 +#define _GUARD_BINARY_OP_EXTEND_RHS_r12 920 +#define _GUARD_BINARY_OP_EXTEND_RHS_r22 921 +#define _GUARD_BINARY_OP_EXTEND_RHS_r33 922 +#define _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r02 923 +#define _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r12 924 +#define _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r22 925 +#define _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r33 926 +#define _GUARD_BIT_IS_SET_POP_r00 927 +#define _GUARD_BIT_IS_SET_POP_r10 928 +#define _GUARD_BIT_IS_SET_POP_r21 929 +#define _GUARD_BIT_IS_SET_POP_r32 930 +#define _GUARD_BIT_IS_SET_POP_4_r00 931 +#define _GUARD_BIT_IS_SET_POP_4_r10 932 +#define _GUARD_BIT_IS_SET_POP_4_r21 933 +#define _GUARD_BIT_IS_SET_POP_4_r32 934 +#define _GUARD_BIT_IS_SET_POP_5_r00 935 +#define _GUARD_BIT_IS_SET_POP_5_r10 936 +#define _GUARD_BIT_IS_SET_POP_5_r21 937 +#define _GUARD_BIT_IS_SET_POP_5_r32 938 +#define _GUARD_BIT_IS_SET_POP_6_r00 939 +#define _GUARD_BIT_IS_SET_POP_6_r10 940 +#define _GUARD_BIT_IS_SET_POP_6_r21 941 +#define _GUARD_BIT_IS_SET_POP_6_r32 942 +#define _GUARD_BIT_IS_SET_POP_7_r00 943 +#define _GUARD_BIT_IS_SET_POP_7_r10 944 +#define _GUARD_BIT_IS_SET_POP_7_r21 945 +#define _GUARD_BIT_IS_SET_POP_7_r32 946 +#define _GUARD_BIT_IS_UNSET_POP_r00 947 +#define _GUARD_BIT_IS_UNSET_POP_r10 948 +#define _GUARD_BIT_IS_UNSET_POP_r21 949 +#define _GUARD_BIT_IS_UNSET_POP_r32 950 +#define _GUARD_BIT_IS_UNSET_POP_4_r00 951 +#define _GUARD_BIT_IS_UNSET_POP_4_r10 952 +#define _GUARD_BIT_IS_UNSET_POP_4_r21 953 +#define _GUARD_BIT_IS_UNSET_POP_4_r32 954 +#define _GUARD_BIT_IS_UNSET_POP_5_r00 955 +#define _GUARD_BIT_IS_UNSET_POP_5_r10 956 +#define _GUARD_BIT_IS_UNSET_POP_5_r21 957 +#define _GUARD_BIT_IS_UNSET_POP_5_r32 958 +#define _GUARD_BIT_IS_UNSET_POP_6_r00 959 +#define _GUARD_BIT_IS_UNSET_POP_6_r10 960 +#define _GUARD_BIT_IS_UNSET_POP_6_r21 961 +#define _GUARD_BIT_IS_UNSET_POP_6_r32 962 +#define _GUARD_BIT_IS_UNSET_POP_7_r00 963 +#define _GUARD_BIT_IS_UNSET_POP_7_r10 964 +#define _GUARD_BIT_IS_UNSET_POP_7_r21 965 +#define _GUARD_BIT_IS_UNSET_POP_7_r32 966 +#define _GUARD_CALLABLE_BUILTIN_CLASS_r00 967 +#define _GUARD_CALLABLE_BUILTIN_FAST_r00 968 +#define _GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS_r00 969 +#define _GUARD_CALLABLE_BUILTIN_O_r00 970 +#define _GUARD_CALLABLE_ISINSTANCE_r03 971 +#define _GUARD_CALLABLE_ISINSTANCE_r13 972 +#define _GUARD_CALLABLE_ISINSTANCE_r23 973 +#define _GUARD_CALLABLE_ISINSTANCE_r33 974 +#define _GUARD_CALLABLE_LEN_r03 975 +#define _GUARD_CALLABLE_LEN_r13 976 +#define _GUARD_CALLABLE_LEN_r23 977 +#define _GUARD_CALLABLE_LEN_r33 978 +#define _GUARD_CALLABLE_LIST_APPEND_r03 979 +#define _GUARD_CALLABLE_LIST_APPEND_r13 980 +#define _GUARD_CALLABLE_LIST_APPEND_r23 981 +#define _GUARD_CALLABLE_LIST_APPEND_r33 982 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_r00 983 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00 984 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS_r00 985 +#define _GUARD_CALLABLE_METHOD_DESCRIPTOR_O_r00 986 +#define _GUARD_CALLABLE_STR_1_r03 987 +#define _GUARD_CALLABLE_STR_1_r13 988 +#define _GUARD_CALLABLE_STR_1_r23 989 +#define _GUARD_CALLABLE_STR_1_r33 990 +#define _GUARD_CALLABLE_TUPLE_1_r03 991 +#define _GUARD_CALLABLE_TUPLE_1_r13 992 +#define _GUARD_CALLABLE_TUPLE_1_r23 993 +#define _GUARD_CALLABLE_TUPLE_1_r33 994 +#define _GUARD_CALLABLE_TYPE_1_r03 995 +#define _GUARD_CALLABLE_TYPE_1_r13 996 +#define _GUARD_CALLABLE_TYPE_1_r23 997 +#define _GUARD_CALLABLE_TYPE_1_r33 998 +#define _GUARD_CODE_VERSION_RETURN_GENERATOR_r00 999 +#define _GUARD_CODE_VERSION_RETURN_GENERATOR_r11 1000 +#define _GUARD_CODE_VERSION_RETURN_GENERATOR_r22 1001 +#define _GUARD_CODE_VERSION_RETURN_GENERATOR_r33 1002 +#define _GUARD_CODE_VERSION_RETURN_VALUE_r00 1003 +#define _GUARD_CODE_VERSION_RETURN_VALUE_r11 1004 +#define _GUARD_CODE_VERSION_RETURN_VALUE_r22 1005 +#define _GUARD_CODE_VERSION_RETURN_VALUE_r33 1006 +#define _GUARD_CODE_VERSION_YIELD_VALUE_r00 1007 +#define _GUARD_CODE_VERSION_YIELD_VALUE_r11 1008 +#define _GUARD_CODE_VERSION_YIELD_VALUE_r22 1009 +#define _GUARD_CODE_VERSION_YIELD_VALUE_r33 1010 +#define _GUARD_CODE_VERSION__PUSH_FRAME_r00 1011 +#define _GUARD_CODE_VERSION__PUSH_FRAME_r11 1012 +#define _GUARD_CODE_VERSION__PUSH_FRAME_r22 1013 +#define _GUARD_CODE_VERSION__PUSH_FRAME_r33 1014 +#define _GUARD_DORV_NO_DICT_r01 1015 +#define _GUARD_DORV_NO_DICT_r11 1016 +#define _GUARD_DORV_NO_DICT_r22 1017 +#define _GUARD_DORV_NO_DICT_r33 1018 +#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r01 1019 +#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r11 1020 +#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r22 1021 +#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r33 1022 +#define _GUARD_GLOBALS_VERSION_r00 1023 +#define _GUARD_GLOBALS_VERSION_r11 1024 +#define _GUARD_GLOBALS_VERSION_r22 1025 +#define _GUARD_GLOBALS_VERSION_r33 1026 +#define _GUARD_IP_RETURN_GENERATOR_r00 1027 +#define _GUARD_IP_RETURN_GENERATOR_r11 1028 +#define _GUARD_IP_RETURN_GENERATOR_r22 1029 +#define _GUARD_IP_RETURN_GENERATOR_r33 1030 +#define _GUARD_IP_RETURN_VALUE_r00 1031 +#define _GUARD_IP_RETURN_VALUE_r11 1032 +#define _GUARD_IP_RETURN_VALUE_r22 1033 +#define _GUARD_IP_RETURN_VALUE_r33 1034 +#define _GUARD_IP_YIELD_VALUE_r00 1035 +#define _GUARD_IP_YIELD_VALUE_r11 1036 +#define _GUARD_IP_YIELD_VALUE_r22 1037 +#define _GUARD_IP_YIELD_VALUE_r33 1038 +#define _GUARD_IP__PUSH_FRAME_r00 1039 +#define _GUARD_IP__PUSH_FRAME_r11 1040 +#define _GUARD_IP__PUSH_FRAME_r22 1041 +#define _GUARD_IP__PUSH_FRAME_r33 1042 +#define _GUARD_IS_FALSE_POP_r00 1043 +#define _GUARD_IS_FALSE_POP_r10 1044 +#define _GUARD_IS_FALSE_POP_r21 1045 +#define _GUARD_IS_FALSE_POP_r32 1046 +#define _GUARD_IS_NONE_POP_r00 1047 +#define _GUARD_IS_NONE_POP_r10 1048 +#define _GUARD_IS_NONE_POP_r21 1049 +#define _GUARD_IS_NONE_POP_r32 1050 +#define _GUARD_IS_NOT_NONE_POP_r10 1051 +#define _GUARD_IS_TRUE_POP_r00 1052 +#define _GUARD_IS_TRUE_POP_r10 1053 +#define _GUARD_IS_TRUE_POP_r21 1054 +#define _GUARD_IS_TRUE_POP_r32 1055 +#define _GUARD_ITERATOR_r01 1056 +#define _GUARD_ITERATOR_r11 1057 +#define _GUARD_ITERATOR_r22 1058 +#define _GUARD_ITERATOR_r33 1059 +#define _GUARD_ITER_VIRTUAL_r01 1060 +#define _GUARD_ITER_VIRTUAL_r11 1061 +#define _GUARD_ITER_VIRTUAL_r22 1062 +#define _GUARD_ITER_VIRTUAL_r33 1063 +#define _GUARD_KEYS_VERSION_r01 1064 +#define _GUARD_KEYS_VERSION_r11 1065 +#define _GUARD_KEYS_VERSION_r22 1066 +#define _GUARD_KEYS_VERSION_r33 1067 +#define _GUARD_LOAD_SUPER_ATTR_METHOD_r03 1068 +#define _GUARD_LOAD_SUPER_ATTR_METHOD_r13 1069 +#define _GUARD_LOAD_SUPER_ATTR_METHOD_r23 1070 +#define _GUARD_LOAD_SUPER_ATTR_METHOD_r33 1071 +#define _GUARD_NOS_COMPACT_ASCII_r02 1072 +#define _GUARD_NOS_COMPACT_ASCII_r12 1073 +#define _GUARD_NOS_COMPACT_ASCII_r22 1074 +#define _GUARD_NOS_COMPACT_ASCII_r33 1075 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r03 1076 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r13 1077 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r23 1078 +#define _GUARD_NOS_DICT_STORE_SUBSCRIPT_r33 1079 +#define _GUARD_NOS_DICT_SUBSCRIPT_r02 1080 +#define _GUARD_NOS_DICT_SUBSCRIPT_r12 1081 +#define _GUARD_NOS_DICT_SUBSCRIPT_r22 1082 +#define _GUARD_NOS_DICT_SUBSCRIPT_r33 1083 +#define _GUARD_NOS_FLOAT_r02 1084 +#define _GUARD_NOS_FLOAT_r12 1085 +#define _GUARD_NOS_FLOAT_r22 1086 +#define _GUARD_NOS_FLOAT_r33 1087 +#define _GUARD_NOS_INT_r02 1088 +#define _GUARD_NOS_INT_r12 1089 +#define _GUARD_NOS_INT_r22 1090 +#define _GUARD_NOS_INT_r33 1091 +#define _GUARD_NOS_ITER_VIRTUAL_r02 1092 +#define _GUARD_NOS_ITER_VIRTUAL_r12 1093 +#define _GUARD_NOS_ITER_VIRTUAL_r22 1094 +#define _GUARD_NOS_ITER_VIRTUAL_r33 1095 +#define _GUARD_NOS_LIST_r02 1096 +#define _GUARD_NOS_LIST_r12 1097 +#define _GUARD_NOS_LIST_r22 1098 +#define _GUARD_NOS_LIST_r33 1099 +#define _GUARD_NOS_NOT_NULL_r02 1100 +#define _GUARD_NOS_NOT_NULL_r12 1101 +#define _GUARD_NOS_NOT_NULL_r22 1102 +#define _GUARD_NOS_NOT_NULL_r33 1103 +#define _GUARD_NOS_NULL_r02 1104 +#define _GUARD_NOS_NULL_r12 1105 +#define _GUARD_NOS_NULL_r22 1106 +#define _GUARD_NOS_NULL_r33 1107 +#define _GUARD_NOS_OVERFLOWED_r02 1108 +#define _GUARD_NOS_OVERFLOWED_r12 1109 +#define _GUARD_NOS_OVERFLOWED_r22 1110 +#define _GUARD_NOS_OVERFLOWED_r33 1111 +#define _GUARD_NOS_TUPLE_r02 1112 +#define _GUARD_NOS_TUPLE_r12 1113 +#define _GUARD_NOS_TUPLE_r22 1114 +#define _GUARD_NOS_TUPLE_r33 1115 +#define _GUARD_NOS_TYPE_VERSION_r02 1116 +#define _GUARD_NOS_TYPE_VERSION_r12 1117 +#define _GUARD_NOS_TYPE_VERSION_r22 1118 +#define _GUARD_NOS_TYPE_VERSION_r33 1119 +#define _GUARD_NOS_UNICODE_r02 1120 +#define _GUARD_NOS_UNICODE_r12 1121 +#define _GUARD_NOS_UNICODE_r22 1122 +#define _GUARD_NOS_UNICODE_r33 1123 +#define _GUARD_NOT_EXHAUSTED_LIST_r02 1124 +#define _GUARD_NOT_EXHAUSTED_LIST_r12 1125 +#define _GUARD_NOT_EXHAUSTED_LIST_r22 1126 +#define _GUARD_NOT_EXHAUSTED_LIST_r33 1127 +#define _GUARD_NOT_EXHAUSTED_RANGE_r02 1128 +#define _GUARD_NOT_EXHAUSTED_RANGE_r12 1129 +#define _GUARD_NOT_EXHAUSTED_RANGE_r22 1130 +#define _GUARD_NOT_EXHAUSTED_RANGE_r33 1131 +#define _GUARD_NOT_EXHAUSTED_TUPLE_r02 1132 +#define _GUARD_NOT_EXHAUSTED_TUPLE_r12 1133 +#define _GUARD_NOT_EXHAUSTED_TUPLE_r22 1134 +#define _GUARD_NOT_EXHAUSTED_TUPLE_r33 1135 +#define _GUARD_THIRD_NULL_r03 1136 +#define _GUARD_THIRD_NULL_r13 1137 +#define _GUARD_THIRD_NULL_r23 1138 +#define _GUARD_THIRD_NULL_r33 1139 +#define _GUARD_TOS_ANY_DICT_r01 1140 +#define _GUARD_TOS_ANY_DICT_r11 1141 +#define _GUARD_TOS_ANY_DICT_r22 1142 +#define _GUARD_TOS_ANY_DICT_r33 1143 +#define _GUARD_TOS_ANY_SET_r01 1144 +#define _GUARD_TOS_ANY_SET_r11 1145 +#define _GUARD_TOS_ANY_SET_r22 1146 +#define _GUARD_TOS_ANY_SET_r33 1147 +#define _GUARD_TOS_DICT_r01 1148 +#define _GUARD_TOS_DICT_r11 1149 +#define _GUARD_TOS_DICT_r22 1150 +#define _GUARD_TOS_DICT_r33 1151 +#define _GUARD_TOS_FLOAT_r01 1152 +#define _GUARD_TOS_FLOAT_r11 1153 +#define _GUARD_TOS_FLOAT_r22 1154 +#define _GUARD_TOS_FLOAT_r33 1155 +#define _GUARD_TOS_FROZENDICT_r01 1156 +#define _GUARD_TOS_FROZENDICT_r11 1157 +#define _GUARD_TOS_FROZENDICT_r22 1158 +#define _GUARD_TOS_FROZENDICT_r33 1159 +#define _GUARD_TOS_FROZENSET_r01 1160 +#define _GUARD_TOS_FROZENSET_r11 1161 +#define _GUARD_TOS_FROZENSET_r22 1162 +#define _GUARD_TOS_FROZENSET_r33 1163 +#define _GUARD_TOS_INT_r01 1164 +#define _GUARD_TOS_INT_r11 1165 +#define _GUARD_TOS_INT_r22 1166 +#define _GUARD_TOS_INT_r33 1167 +#define _GUARD_TOS_IS_NONE_r01 1168 +#define _GUARD_TOS_IS_NONE_r11 1169 +#define _GUARD_TOS_IS_NONE_r22 1170 +#define _GUARD_TOS_IS_NONE_r33 1171 +#define _GUARD_TOS_LIST_r01 1172 +#define _GUARD_TOS_LIST_r11 1173 +#define _GUARD_TOS_LIST_r22 1174 +#define _GUARD_TOS_LIST_r33 1175 +#define _GUARD_TOS_NOT_NULL_r01 1176 +#define _GUARD_TOS_NOT_NULL_r11 1177 +#define _GUARD_TOS_NOT_NULL_r22 1178 +#define _GUARD_TOS_NOT_NULL_r33 1179 +#define _GUARD_TOS_OVERFLOWED_r01 1180 +#define _GUARD_TOS_OVERFLOWED_r11 1181 +#define _GUARD_TOS_OVERFLOWED_r22 1182 +#define _GUARD_TOS_OVERFLOWED_r33 1183 +#define _GUARD_TOS_SET_r01 1184 +#define _GUARD_TOS_SET_r11 1185 +#define _GUARD_TOS_SET_r22 1186 +#define _GUARD_TOS_SET_r33 1187 +#define _GUARD_TOS_SLICE_r01 1188 +#define _GUARD_TOS_SLICE_r11 1189 +#define _GUARD_TOS_SLICE_r22 1190 +#define _GUARD_TOS_SLICE_r33 1191 +#define _GUARD_TOS_TUPLE_r01 1192 +#define _GUARD_TOS_TUPLE_r11 1193 +#define _GUARD_TOS_TUPLE_r22 1194 +#define _GUARD_TOS_TUPLE_r33 1195 +#define _GUARD_TOS_UNICODE_r01 1196 +#define _GUARD_TOS_UNICODE_r11 1197 +#define _GUARD_TOS_UNICODE_r22 1198 +#define _GUARD_TOS_UNICODE_r33 1199 +#define _GUARD_TYPE_r01 1200 +#define _GUARD_TYPE_r11 1201 +#define _GUARD_TYPE_r22 1202 +#define _GUARD_TYPE_r33 1203 +#define _GUARD_TYPE_ITER_r02 1204 +#define _GUARD_TYPE_ITER_r12 1205 +#define _GUARD_TYPE_ITER_r22 1206 +#define _GUARD_TYPE_ITER_r33 1207 +#define _GUARD_TYPE_VERSION_r01 1208 +#define _GUARD_TYPE_VERSION_r11 1209 +#define _GUARD_TYPE_VERSION_r22 1210 +#define _GUARD_TYPE_VERSION_r33 1211 +#define _GUARD_TYPE_VERSION_LOCKED_r01 1212 +#define _GUARD_TYPE_VERSION_LOCKED_r11 1213 +#define _GUARD_TYPE_VERSION_LOCKED_r22 1214 +#define _GUARD_TYPE_VERSION_LOCKED_r33 1215 +#define _HANDLE_PENDING_AND_DEOPT_r00 1216 +#define _HANDLE_PENDING_AND_DEOPT_r10 1217 +#define _HANDLE_PENDING_AND_DEOPT_r20 1218 +#define _HANDLE_PENDING_AND_DEOPT_r30 1219 +#define _IMPORT_FROM_r12 1220 +#define _IMPORT_NAME_r21 1221 +#define _INIT_CALL_BOUND_METHOD_EXACT_ARGS_r00 1222 +#define _INIT_CALL_PY_EXACT_ARGS_r01 1223 +#define _INIT_CALL_PY_EXACT_ARGS_0_r01 1224 +#define _INIT_CALL_PY_EXACT_ARGS_1_r01 1225 +#define _INIT_CALL_PY_EXACT_ARGS_2_r01 1226 +#define _INIT_CALL_PY_EXACT_ARGS_3_r01 1227 +#define _INIT_CALL_PY_EXACT_ARGS_4_r01 1228 +#define _INSERT_NULL_r10 1229 +#define _INSTRUMENTED_FOR_ITER_r23 1230 +#define _INSTRUMENTED_INSTRUCTION_r00 1231 +#define _INSTRUMENTED_JUMP_FORWARD_r00 1232 +#define _INSTRUMENTED_JUMP_FORWARD_r11 1233 +#define _INSTRUMENTED_JUMP_FORWARD_r22 1234 +#define _INSTRUMENTED_JUMP_FORWARD_r33 1235 +#define _INSTRUMENTED_LINE_r00 1236 +#define _INSTRUMENTED_NOT_TAKEN_r00 1237 +#define _INSTRUMENTED_NOT_TAKEN_r11 1238 +#define _INSTRUMENTED_NOT_TAKEN_r22 1239 +#define _INSTRUMENTED_NOT_TAKEN_r33 1240 +#define _INSTRUMENTED_POP_JUMP_IF_FALSE_r00 1241 +#define _INSTRUMENTED_POP_JUMP_IF_FALSE_r10 1242 +#define _INSTRUMENTED_POP_JUMP_IF_FALSE_r21 1243 +#define _INSTRUMENTED_POP_JUMP_IF_FALSE_r32 1244 +#define _INSTRUMENTED_POP_JUMP_IF_NONE_r10 1245 +#define _INSTRUMENTED_POP_JUMP_IF_NOT_NONE_r10 1246 +#define _INSTRUMENTED_POP_JUMP_IF_TRUE_r00 1247 +#define _INSTRUMENTED_POP_JUMP_IF_TRUE_r10 1248 +#define _INSTRUMENTED_POP_JUMP_IF_TRUE_r21 1249 +#define _INSTRUMENTED_POP_JUMP_IF_TRUE_r32 1250 +#define _IS_NONE_r11 1251 +#define _IS_OP_r03 1252 +#define _IS_OP_r13 1253 +#define _IS_OP_r23 1254 +#define _ITER_CHECK_LIST_r02 1255 +#define _ITER_CHECK_LIST_r12 1256 +#define _ITER_CHECK_LIST_r22 1257 +#define _ITER_CHECK_LIST_r33 1258 +#define _ITER_CHECK_RANGE_r02 1259 +#define _ITER_CHECK_RANGE_r12 1260 +#define _ITER_CHECK_RANGE_r22 1261 +#define _ITER_CHECK_RANGE_r33 1262 +#define _ITER_CHECK_TUPLE_r02 1263 +#define _ITER_CHECK_TUPLE_r12 1264 +#define _ITER_CHECK_TUPLE_r22 1265 +#define _ITER_CHECK_TUPLE_r33 1266 +#define _ITER_JUMP_LIST_r02 1267 +#define _ITER_JUMP_LIST_r12 1268 +#define _ITER_JUMP_LIST_r22 1269 +#define _ITER_JUMP_LIST_r33 1270 +#define _ITER_JUMP_RANGE_r02 1271 +#define _ITER_JUMP_RANGE_r12 1272 +#define _ITER_JUMP_RANGE_r22 1273 +#define _ITER_JUMP_RANGE_r33 1274 +#define _ITER_JUMP_TUPLE_r02 1275 +#define _ITER_JUMP_TUPLE_r12 1276 +#define _ITER_JUMP_TUPLE_r22 1277 +#define _ITER_JUMP_TUPLE_r33 1278 +#define _ITER_NEXT_INLINE_r23 1279 +#define _ITER_NEXT_LIST_r23 1280 +#define _ITER_NEXT_LIST_TIER_TWO_r23 1281 +#define _ITER_NEXT_RANGE_r03 1282 +#define _ITER_NEXT_RANGE_r13 1283 +#define _ITER_NEXT_RANGE_r23 1284 +#define _ITER_NEXT_TUPLE_r03 1285 +#define _ITER_NEXT_TUPLE_r13 1286 +#define _ITER_NEXT_TUPLE_r23 1287 +#define _JUMP_BACKWARD_NO_INTERRUPT_r00 1288 +#define _JUMP_BACKWARD_NO_INTERRUPT_r11 1289 +#define _JUMP_BACKWARD_NO_INTERRUPT_r22 1290 +#define _JUMP_BACKWARD_NO_INTERRUPT_r33 1291 +#define _JUMP_TO_TOP_r00 1292 +#define _LIST_APPEND_r10 1293 +#define _LIST_EXTEND_r11 1294 +#define _LOAD_ATTR_r10 1295 +#define _LOAD_ATTR_CLASS_r11 1296 +#define _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME_r11 1297 +#define _LOAD_ATTR_INSTANCE_VALUE_r02 1298 +#define _LOAD_ATTR_INSTANCE_VALUE_r12 1299 +#define _LOAD_ATTR_INSTANCE_VALUE_r23 1300 +#define _LOAD_ATTR_METHOD_LAZY_DICT_r02 1301 +#define _LOAD_ATTR_METHOD_LAZY_DICT_r12 1302 +#define _LOAD_ATTR_METHOD_LAZY_DICT_r23 1303 +#define _LOAD_ATTR_METHOD_NO_DICT_r02 1304 +#define _LOAD_ATTR_METHOD_NO_DICT_r12 1305 +#define _LOAD_ATTR_METHOD_NO_DICT_r23 1306 +#define _LOAD_ATTR_METHOD_WITH_VALUES_r02 1307 +#define _LOAD_ATTR_METHOD_WITH_VALUES_r12 1308 +#define _LOAD_ATTR_METHOD_WITH_VALUES_r23 1309 +#define _LOAD_ATTR_MODULE_r12 1310 +#define _LOAD_ATTR_NONDESCRIPTOR_NO_DICT_r11 1311 +#define _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES_r11 1312 +#define _LOAD_ATTR_PROPERTY_FRAME_r01 1313 +#define _LOAD_ATTR_PROPERTY_FRAME_r11 1314 +#define _LOAD_ATTR_PROPERTY_FRAME_r22 1315 +#define _LOAD_ATTR_PROPERTY_FRAME_r33 1316 +#define _LOAD_ATTR_SLOT_r02 1317 +#define _LOAD_ATTR_SLOT_r12 1318 +#define _LOAD_ATTR_SLOT_r23 1319 +#define _LOAD_ATTR_WITH_HINT_r12 1320 +#define _LOAD_BUILD_CLASS_r01 1321 +#define _LOAD_BYTECODE_r00 1322 +#define _LOAD_COMMON_CONSTANT_r01 1323 +#define _LOAD_COMMON_CONSTANT_r12 1324 +#define _LOAD_COMMON_CONSTANT_r23 1325 +#define _LOAD_CONST_r01 1326 +#define _LOAD_CONST_r12 1327 +#define _LOAD_CONST_r23 1328 +#define _LOAD_CONST_INLINE_r01 1329 +#define _LOAD_CONST_INLINE_r12 1330 +#define _LOAD_CONST_INLINE_r23 1331 +#define _LOAD_CONST_INLINE_BORROW_r01 1332 +#define _LOAD_CONST_INLINE_BORROW_r12 1333 +#define _LOAD_CONST_INLINE_BORROW_r23 1334 +#define _LOAD_DEREF_r01 1335 +#define _LOAD_FAST_r01 1336 +#define _LOAD_FAST_r12 1337 +#define _LOAD_FAST_r23 1338 +#define _LOAD_FAST_0_r01 1339 +#define _LOAD_FAST_0_r12 1340 +#define _LOAD_FAST_0_r23 1341 +#define _LOAD_FAST_1_r01 1342 +#define _LOAD_FAST_1_r12 1343 +#define _LOAD_FAST_1_r23 1344 +#define _LOAD_FAST_2_r01 1345 +#define _LOAD_FAST_2_r12 1346 +#define _LOAD_FAST_2_r23 1347 +#define _LOAD_FAST_3_r01 1348 +#define _LOAD_FAST_3_r12 1349 +#define _LOAD_FAST_3_r23 1350 +#define _LOAD_FAST_4_r01 1351 +#define _LOAD_FAST_4_r12 1352 +#define _LOAD_FAST_4_r23 1353 +#define _LOAD_FAST_5_r01 1354 +#define _LOAD_FAST_5_r12 1355 +#define _LOAD_FAST_5_r23 1356 +#define _LOAD_FAST_6_r01 1357 +#define _LOAD_FAST_6_r12 1358 +#define _LOAD_FAST_6_r23 1359 +#define _LOAD_FAST_7_r01 1360 +#define _LOAD_FAST_7_r12 1361 +#define _LOAD_FAST_7_r23 1362 +#define _LOAD_FAST_AND_CLEAR_r01 1363 +#define _LOAD_FAST_AND_CLEAR_r12 1364 +#define _LOAD_FAST_AND_CLEAR_r23 1365 +#define _LOAD_FAST_BORROW_r01 1366 +#define _LOAD_FAST_BORROW_r12 1367 +#define _LOAD_FAST_BORROW_r23 1368 +#define _LOAD_FAST_BORROW_0_r01 1369 +#define _LOAD_FAST_BORROW_0_r12 1370 +#define _LOAD_FAST_BORROW_0_r23 1371 +#define _LOAD_FAST_BORROW_1_r01 1372 +#define _LOAD_FAST_BORROW_1_r12 1373 +#define _LOAD_FAST_BORROW_1_r23 1374 +#define _LOAD_FAST_BORROW_2_r01 1375 +#define _LOAD_FAST_BORROW_2_r12 1376 +#define _LOAD_FAST_BORROW_2_r23 1377 +#define _LOAD_FAST_BORROW_3_r01 1378 +#define _LOAD_FAST_BORROW_3_r12 1379 +#define _LOAD_FAST_BORROW_3_r23 1380 +#define _LOAD_FAST_BORROW_4_r01 1381 +#define _LOAD_FAST_BORROW_4_r12 1382 +#define _LOAD_FAST_BORROW_4_r23 1383 +#define _LOAD_FAST_BORROW_5_r01 1384 +#define _LOAD_FAST_BORROW_5_r12 1385 +#define _LOAD_FAST_BORROW_5_r23 1386 +#define _LOAD_FAST_BORROW_6_r01 1387 +#define _LOAD_FAST_BORROW_6_r12 1388 +#define _LOAD_FAST_BORROW_6_r23 1389 +#define _LOAD_FAST_BORROW_7_r01 1390 +#define _LOAD_FAST_BORROW_7_r12 1391 +#define _LOAD_FAST_BORROW_7_r23 1392 +#define _LOAD_FAST_BORROW_LOAD_FAST_BORROW_r02 1393 +#define _LOAD_FAST_BORROW_LOAD_FAST_BORROW_r13 1394 +#define _LOAD_FAST_CHECK_r01 1395 +#define _LOAD_FAST_CHECK_r12 1396 +#define _LOAD_FAST_CHECK_r23 1397 +#define _LOAD_FAST_LOAD_FAST_r02 1398 +#define _LOAD_FAST_LOAD_FAST_r13 1399 +#define _LOAD_FROM_DICT_OR_DEREF_r11 1400 +#define _LOAD_FROM_DICT_OR_GLOBALS_r11 1401 +#define _LOAD_GLOBAL_r00 1402 +#define _LOAD_GLOBAL_BUILTINS_r01 1403 +#define _LOAD_GLOBAL_MODULE_r01 1404 +#define _LOAD_LOCALS_r01 1405 +#define _LOAD_LOCALS_r12 1406 +#define _LOAD_LOCALS_r23 1407 +#define _LOAD_NAME_r01 1408 +#define _LOAD_SMALL_INT_r01 1409 +#define _LOAD_SMALL_INT_r12 1410 +#define _LOAD_SMALL_INT_r23 1411 +#define _LOAD_SMALL_INT_0_r01 1412 +#define _LOAD_SMALL_INT_0_r12 1413 +#define _LOAD_SMALL_INT_0_r23 1414 +#define _LOAD_SMALL_INT_1_r01 1415 +#define _LOAD_SMALL_INT_1_r12 1416 +#define _LOAD_SMALL_INT_1_r23 1417 +#define _LOAD_SMALL_INT_2_r01 1418 +#define _LOAD_SMALL_INT_2_r12 1419 +#define _LOAD_SMALL_INT_2_r23 1420 +#define _LOAD_SMALL_INT_3_r01 1421 +#define _LOAD_SMALL_INT_3_r12 1422 +#define _LOAD_SMALL_INT_3_r23 1423 +#define _LOAD_SPECIAL_r00 1424 +#define _LOAD_SUPER_ATTR_ATTR_r31 1425 +#define _LOAD_SUPER_ATTR_METHOD_r32 1426 +#define _LOCK_OBJECT_r01 1427 +#define _LOCK_OBJECT_r11 1428 +#define _LOCK_OBJECT_r22 1429 +#define _LOCK_OBJECT_r33 1430 +#define _MAKE_CALLARGS_A_TUPLE_r33 1431 +#define _MAKE_CELL_r00 1432 +#define _MAKE_FUNCTION_r12 1433 +#define _MAKE_HEAP_SAFE_r01 1434 +#define _MAKE_HEAP_SAFE_r11 1435 +#define _MAKE_HEAP_SAFE_r22 1436 +#define _MAKE_HEAP_SAFE_r33 1437 +#define _MAKE_WARM_r00 1438 +#define _MAKE_WARM_r11 1439 +#define _MAKE_WARM_r22 1440 +#define _MAKE_WARM_r33 1441 +#define _MAP_ADD_r20 1442 +#define _MATCH_CLASS_r33 1443 +#define _MATCH_KEYS_r23 1444 +#define _MATCH_MAPPING_r02 1445 +#define _MATCH_MAPPING_r12 1446 +#define _MATCH_MAPPING_r23 1447 +#define _MATCH_SEQUENCE_r02 1448 +#define _MATCH_SEQUENCE_r12 1449 +#define _MATCH_SEQUENCE_r23 1450 +#define _MAYBE_EXPAND_METHOD_r00 1451 +#define _MAYBE_EXPAND_METHOD_KW_r11 1452 +#define _MONITOR_CALL_r00 1453 +#define _MONITOR_CALL_KW_r11 1454 +#define _MONITOR_JUMP_BACKWARD_r00 1455 +#define _MONITOR_JUMP_BACKWARD_r11 1456 +#define _MONITOR_JUMP_BACKWARD_r22 1457 +#define _MONITOR_JUMP_BACKWARD_r33 1458 +#define _MONITOR_RESUME_r00 1459 +#define _NOP_r00 1460 +#define _NOP_r11 1461 +#define _NOP_r22 1462 +#define _NOP_r33 1463 +#define _POP_EXCEPT_r10 1464 +#define _POP_ITER_r20 1465 +#define _POP_JUMP_IF_FALSE_r00 1466 +#define _POP_JUMP_IF_FALSE_r10 1467 +#define _POP_JUMP_IF_FALSE_r21 1468 +#define _POP_JUMP_IF_FALSE_r32 1469 +#define _POP_JUMP_IF_TRUE_r00 1470 +#define _POP_JUMP_IF_TRUE_r10 1471 +#define _POP_JUMP_IF_TRUE_r21 1472 +#define _POP_JUMP_IF_TRUE_r32 1473 +#define _POP_TOP_r10 1474 +#define _POP_TOP_FLOAT_r00 1475 +#define _POP_TOP_FLOAT_r10 1476 +#define _POP_TOP_FLOAT_r21 1477 +#define _POP_TOP_FLOAT_r32 1478 +#define _POP_TOP_INT_r00 1479 +#define _POP_TOP_INT_r10 1480 +#define _POP_TOP_INT_r21 1481 +#define _POP_TOP_INT_r32 1482 +#define _POP_TOP_NOP_r00 1483 +#define _POP_TOP_NOP_r10 1484 +#define _POP_TOP_NOP_r21 1485 +#define _POP_TOP_NOP_r32 1486 +#define _POP_TOP_OPARG_r00 1487 +#define _POP_TOP_UNICODE_r00 1488 +#define _POP_TOP_UNICODE_r10 1489 +#define _POP_TOP_UNICODE_r21 1490 +#define _POP_TOP_UNICODE_r32 1491 +#define _PUSH_EXC_INFO_r02 1492 +#define _PUSH_EXC_INFO_r12 1493 +#define _PUSH_EXC_INFO_r23 1494 +#define _PUSH_FRAME_r10 1495 +#define _PUSH_NULL_r01 1496 +#define _PUSH_NULL_r12 1497 +#define _PUSH_NULL_r23 1498 +#define _PUSH_NULL_CONDITIONAL_r00 1499 +#define _PUSH_TAGGED_ZERO_r01 1500 +#define _PUSH_TAGGED_ZERO_r12 1501 +#define _PUSH_TAGGED_ZERO_r23 1502 +#define _PY_FRAME_EX_r31 1503 +#define _PY_FRAME_GENERAL_r01 1504 +#define _PY_FRAME_KW_r11 1505 +#define _REPLACE_WITH_TRUE_r02 1506 +#define _REPLACE_WITH_TRUE_r12 1507 +#define _REPLACE_WITH_TRUE_r23 1508 +#define _RESUME_CHECK_r00 1509 +#define _RESUME_CHECK_r11 1510 +#define _RESUME_CHECK_r22 1511 +#define _RESUME_CHECK_r33 1512 +#define _RETURN_GENERATOR_r01 1513 +#define _RETURN_VALUE_r11 1514 +#define _RROT_3_r03 1515 +#define _RROT_3_r13 1516 +#define _RROT_3_r23 1517 +#define _RROT_3_r33 1518 +#define _SAVE_RETURN_OFFSET_r00 1519 +#define _SAVE_RETURN_OFFSET_r11 1520 +#define _SAVE_RETURN_OFFSET_r22 1521 +#define _SAVE_RETURN_OFFSET_r33 1522 +#define _SEND_ASYNC_GEN_r33 1523 +#define _SEND_ASYNC_GEN_TIER_TWO_r33 1524 +#define _SEND_GEN_FRAME_r33 1525 +#define _SEND_VIRTUAL_r33 1526 +#define _SEND_VIRTUAL_TIER_TWO_r03 1527 +#define _SEND_VIRTUAL_TIER_TWO_r13 1528 +#define _SEND_VIRTUAL_TIER_TWO_r23 1529 +#define _SEND_VIRTUAL_TIER_TWO_r33 1530 +#define _SETUP_ANNOTATIONS_r00 1531 +#define _SET_ADD_r10 1532 +#define _SET_FUNCTION_ATTRIBUTE_r01 1533 +#define _SET_FUNCTION_ATTRIBUTE_r11 1534 +#define _SET_FUNCTION_ATTRIBUTE_r21 1535 +#define _SET_FUNCTION_ATTRIBUTE_r32 1536 +#define _SET_IP_r00 1537 +#define _SET_IP_r11 1538 +#define _SET_IP_r22 1539 +#define _SET_IP_r33 1540 +#define _SET_UPDATE_r11 1541 +#define _SPILL_OR_RELOAD_r01 1542 +#define _SPILL_OR_RELOAD_r02 1543 +#define _SPILL_OR_RELOAD_r03 1544 +#define _SPILL_OR_RELOAD_r10 1545 +#define _SPILL_OR_RELOAD_r12 1546 +#define _SPILL_OR_RELOAD_r13 1547 +#define _SPILL_OR_RELOAD_r20 1548 +#define _SPILL_OR_RELOAD_r21 1549 +#define _SPILL_OR_RELOAD_r23 1550 +#define _SPILL_OR_RELOAD_r30 1551 +#define _SPILL_OR_RELOAD_r31 1552 +#define _SPILL_OR_RELOAD_r32 1553 +#define _START_EXECUTOR_r00 1554 +#define _STORE_ATTR_r20 1555 +#define _STORE_ATTR_INSTANCE_VALUE_r21 1556 +#define _STORE_ATTR_SLOT_r21 1557 +#define _STORE_ATTR_WITH_HINT_r21 1558 +#define _STORE_DEREF_r10 1559 +#define _STORE_FAST_LOAD_FAST_r11 1560 +#define _STORE_FAST_STORE_FAST_r20 1561 +#define _STORE_GLOBAL_r10 1562 +#define _STORE_NAME_r10 1563 +#define _STORE_SLICE_r30 1564 +#define _STORE_SUBSCR_r30 1565 +#define _STORE_SUBSCR_DICT_r31 1566 +#define _STORE_SUBSCR_DICT_KNOWN_HASH_r31 1567 +#define _STORE_SUBSCR_LIST_INT_r32 1568 +#define _SWAP_r11 1569 +#define _SWAP_2_r02 1570 +#define _SWAP_2_r12 1571 +#define _SWAP_2_r22 1572 +#define _SWAP_2_r33 1573 +#define _SWAP_3_r03 1574 +#define _SWAP_3_r13 1575 +#define _SWAP_3_r23 1576 +#define _SWAP_3_r33 1577 +#define _SWAP_FAST_r01 1578 +#define _SWAP_FAST_r11 1579 +#define _SWAP_FAST_r22 1580 +#define _SWAP_FAST_r33 1581 +#define _SWAP_FAST_0_r01 1582 +#define _SWAP_FAST_0_r11 1583 +#define _SWAP_FAST_0_r22 1584 +#define _SWAP_FAST_0_r33 1585 +#define _SWAP_FAST_1_r01 1586 +#define _SWAP_FAST_1_r11 1587 +#define _SWAP_FAST_1_r22 1588 +#define _SWAP_FAST_1_r33 1589 +#define _SWAP_FAST_2_r01 1590 +#define _SWAP_FAST_2_r11 1591 +#define _SWAP_FAST_2_r22 1592 +#define _SWAP_FAST_2_r33 1593 +#define _SWAP_FAST_3_r01 1594 +#define _SWAP_FAST_3_r11 1595 +#define _SWAP_FAST_3_r22 1596 +#define _SWAP_FAST_3_r33 1597 +#define _SWAP_FAST_4_r01 1598 +#define _SWAP_FAST_4_r11 1599 +#define _SWAP_FAST_4_r22 1600 +#define _SWAP_FAST_4_r33 1601 +#define _SWAP_FAST_5_r01 1602 +#define _SWAP_FAST_5_r11 1603 +#define _SWAP_FAST_5_r22 1604 +#define _SWAP_FAST_5_r33 1605 +#define _SWAP_FAST_6_r01 1606 +#define _SWAP_FAST_6_r11 1607 +#define _SWAP_FAST_6_r22 1608 +#define _SWAP_FAST_6_r33 1609 +#define _SWAP_FAST_7_r01 1610 +#define _SWAP_FAST_7_r11 1611 +#define _SWAP_FAST_7_r22 1612 +#define _SWAP_FAST_7_r33 1613 +#define _TIER2_RESUME_CHECK_r00 1614 +#define _TIER2_RESUME_CHECK_r11 1615 +#define _TIER2_RESUME_CHECK_r22 1616 +#define _TIER2_RESUME_CHECK_r33 1617 +#define _TO_BOOL_r11 1618 +#define _TO_BOOL_BOOL_r01 1619 +#define _TO_BOOL_BOOL_r11 1620 +#define _TO_BOOL_BOOL_r22 1621 +#define _TO_BOOL_BOOL_r33 1622 +#define _TO_BOOL_INT_r02 1623 +#define _TO_BOOL_INT_r12 1624 +#define _TO_BOOL_INT_r23 1625 +#define _TO_BOOL_LIST_r02 1626 +#define _TO_BOOL_LIST_r12 1627 +#define _TO_BOOL_LIST_r23 1628 +#define _TO_BOOL_NONE_r01 1629 +#define _TO_BOOL_NONE_r11 1630 +#define _TO_BOOL_NONE_r22 1631 +#define _TO_BOOL_NONE_r33 1632 +#define _TO_BOOL_STR_r02 1633 +#define _TO_BOOL_STR_r12 1634 +#define _TO_BOOL_STR_r23 1635 +#define _TRACE_RECORD_r00 1636 +#define _UNARY_INVERT_r12 1637 +#define _UNARY_NEGATIVE_r12 1638 +#define _UNARY_NEGATIVE_FLOAT_INPLACE_r02 1639 +#define _UNARY_NEGATIVE_FLOAT_INPLACE_r12 1640 +#define _UNARY_NEGATIVE_FLOAT_INPLACE_r23 1641 +#define _UNARY_NOT_r01 1642 +#define _UNARY_NOT_r11 1643 +#define _UNARY_NOT_r22 1644 +#define _UNARY_NOT_r33 1645 +#define _UNPACK_EX_r10 1646 +#define _UNPACK_SEQUENCE_r10 1647 +#define _UNPACK_SEQUENCE_LIST_r10 1648 +#define _UNPACK_SEQUENCE_TUPLE_r10 1649 +#define _UNPACK_SEQUENCE_TWO_TUPLE_r12 1650 +#define _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r03 1651 +#define _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r13 1652 +#define _UNPACK_SEQUENCE_UNIQUE_TUPLE_r10 1653 +#define _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r02 1654 +#define _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r12 1655 +#define _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r23 1656 +#define _WITH_EXCEPT_START_r33 1657 +#define _YIELD_VALUE_r11 1658 +#define MAX_UOP_REGS_ID 1658 #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_metadata.h b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_metadata.h index efdd9395d..a61df9c29 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_metadata.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/internal/pycore_uop_metadata.h @@ -11,19 +11,35 @@ extern "C" { #include #include "pycore_uop_ids.h" -extern const uint16_t _PyUop_Flags[MAX_UOP_ID+1]; -extern const uint8_t _PyUop_Replication[MAX_UOP_ID+1]; -extern const char * const _PyOpcode_uop_name[MAX_UOP_ID+1]; +#define MAX_CACHED_REGISTER 3 +extern const uint32_t _PyUop_Flags[MAX_UOP_ID+1]; +typedef struct _rep_range { uint8_t start; uint8_t stop; } ReplicationRange; +extern const ReplicationRange _PyUop_Replication[MAX_UOP_ID+1]; +extern const char * const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1]; extern int _PyUop_num_popped(int opcode, int oparg); +typedef struct _pyuop_tos_cache_entry { + /* input depth is implicit in position */ + int8_t output; + int8_t exit; + int16_t opcode; +} _PyUopTOSentry; +typedef struct _PyUopCachingInfo { + uint8_t best[MAX_CACHED_REGISTER + 1]; + _PyUopTOSentry entries[MAX_CACHED_REGISTER + 1]; +} _PyUopCachingInfo; +extern const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1]; +extern const uint16_t _PyUop_SpillsAndReloads[4][4]; +extern const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1]; + #ifdef NEED_OPCODE_METADATA -const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = { +const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = { [_NOP] = HAS_PURE_FLAG, [_CHECK_PERIODIC] = HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_CHECK_PERIODIC_IF_NOT_YIELD_FROM] = HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_RESUME_CHECK] = HAS_DEOPT_FLAG, - [_LOAD_FAST_CHECK] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_LOAD_FAST_CHECK] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ERROR_FLAG, [_LOAD_FAST_0] = HAS_LOCAL_FLAG | HAS_PURE_FLAG, [_LOAD_FAST_1] = HAS_LOCAL_FLAG | HAS_PURE_FLAG, [_LOAD_FAST_2] = HAS_LOCAL_FLAG | HAS_PURE_FLAG, @@ -43,101 +59,140 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = { [_LOAD_FAST_BORROW_7] = HAS_LOCAL_FLAG | HAS_PURE_FLAG, [_LOAD_FAST_BORROW] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_PURE_FLAG, [_LOAD_FAST_AND_CLEAR] = HAS_ARG_FLAG | HAS_LOCAL_FLAG, - [_LOAD_FAST_LOAD_FAST] = HAS_ARG_FLAG | HAS_LOCAL_FLAG, - [_LOAD_FAST_BORROW_LOAD_FAST_BORROW] = HAS_ARG_FLAG | HAS_LOCAL_FLAG, - [_LOAD_CONST_MORTAL] = HAS_ARG_FLAG | HAS_CONST_FLAG, - [_LOAD_CONST_IMMORTAL] = HAS_ARG_FLAG | HAS_CONST_FLAG, + [_LOAD_CONST] = HAS_ARG_FLAG | HAS_CONST_FLAG, [_LOAD_SMALL_INT_0] = 0, [_LOAD_SMALL_INT_1] = 0, [_LOAD_SMALL_INT_2] = 0, [_LOAD_SMALL_INT_3] = 0, [_LOAD_SMALL_INT] = HAS_ARG_FLAG, - [_STORE_FAST_0] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_1] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_2] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_3] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_4] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_5] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_6] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_7] = HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_LOAD_FAST] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, - [_STORE_FAST_STORE_FAST] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ESCAPES_FLAG, + [_SWAP_FAST_0] = HAS_LOCAL_FLAG, + [_SWAP_FAST_1] = HAS_LOCAL_FLAG, + [_SWAP_FAST_2] = HAS_LOCAL_FLAG, + [_SWAP_FAST_3] = HAS_LOCAL_FLAG, + [_SWAP_FAST_4] = HAS_LOCAL_FLAG, + [_SWAP_FAST_5] = HAS_LOCAL_FLAG, + [_SWAP_FAST_6] = HAS_LOCAL_FLAG, + [_SWAP_FAST_7] = HAS_LOCAL_FLAG, + [_SWAP_FAST] = HAS_ARG_FLAG | HAS_LOCAL_FLAG, [_POP_TOP] = HAS_ESCAPES_FLAG | HAS_PURE_FLAG, + [_POP_TOP_NOP] = 0, + [_POP_TOP_INT] = 0, + [_POP_TOP_FLOAT] = 0, + [_POP_TOP_UNICODE] = 0, + [_POP_TOP_OPARG] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, [_PUSH_NULL] = HAS_PURE_FLAG, [_END_FOR] = HAS_ESCAPES_FLAG | HAS_NO_SAVE_IP_FLAG, + [_POP_ITER] = HAS_ESCAPES_FLAG, [_END_SEND] = HAS_ESCAPES_FLAG | HAS_PURE_FLAG, - [_UNARY_NEGATIVE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_UNARY_NOT] = HAS_PURE_FLAG, + [_UNARY_NEGATIVE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_UNARY_NEGATIVE_FLOAT_INPLACE] = 0, + [_UNARY_NOT] = 0, [_TO_BOOL] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_TO_BOOL_BOOL] = HAS_EXIT_FLAG, - [_TO_BOOL_INT] = HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, + [_TO_BOOL_INT] = 0, [_GUARD_NOS_LIST] = HAS_EXIT_FLAG, [_GUARD_TOS_LIST] = HAS_EXIT_FLAG, [_GUARD_TOS_SLICE] = HAS_EXIT_FLAG, [_TO_BOOL_LIST] = 0, [_TO_BOOL_NONE] = HAS_EXIT_FLAG, + [_GUARD_NOS_COMPACT_ASCII] = HAS_EXIT_FLAG, [_GUARD_NOS_UNICODE] = HAS_EXIT_FLAG, [_GUARD_TOS_UNICODE] = HAS_EXIT_FLAG, - [_TO_BOOL_STR] = HAS_ESCAPES_FLAG, - [_REPLACE_WITH_TRUE] = HAS_ESCAPES_FLAG, - [_UNARY_INVERT] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_TO_BOOL_STR] = 0, + [_REPLACE_WITH_TRUE] = 0, + [_UNARY_INVERT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_GUARD_NOS_INT] = HAS_EXIT_FLAG, [_GUARD_TOS_INT] = HAS_EXIT_FLAG, - [_BINARY_OP_MULTIPLY_INT] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_PURE_FLAG, - [_BINARY_OP_ADD_INT] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_PURE_FLAG, - [_BINARY_OP_SUBTRACT_INT] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_PURE_FLAG, + [_GUARD_NOS_OVERFLOWED] = HAS_EXIT_FLAG, + [_GUARD_TOS_OVERFLOWED] = HAS_EXIT_FLAG, + [_BINARY_OP_MULTIPLY_INT] = HAS_EXIT_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_ADD_INT] = HAS_EXIT_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_SUBTRACT_INT] = HAS_EXIT_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_ADD_INT_INPLACE] = HAS_EXIT_FLAG, + [_BINARY_OP_SUBTRACT_INT_INPLACE] = HAS_EXIT_FLAG, + [_BINARY_OP_MULTIPLY_INT_INPLACE] = HAS_EXIT_FLAG, + [_BINARY_OP_ADD_INT_INPLACE_RIGHT] = HAS_EXIT_FLAG, + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT] = HAS_EXIT_FLAG, + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT] = HAS_EXIT_FLAG, [_GUARD_NOS_FLOAT] = HAS_EXIT_FLAG, [_GUARD_TOS_FLOAT] = HAS_EXIT_FLAG, - [_BINARY_OP_MULTIPLY_FLOAT] = HAS_ERROR_FLAG | HAS_PURE_FLAG, - [_BINARY_OP_ADD_FLOAT] = HAS_ERROR_FLAG | HAS_PURE_FLAG, - [_BINARY_OP_SUBTRACT_FLOAT] = HAS_ERROR_FLAG | HAS_PURE_FLAG, - [_BINARY_OP_ADD_UNICODE] = HAS_ERROR_FLAG | HAS_PURE_FLAG, - [_BINARY_OP_INPLACE_ADD_UNICODE] = HAS_LOCAL_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_GUARD_BINARY_OP_EXTEND] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, - [_BINARY_OP_EXTEND] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_MULTIPLY_FLOAT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_ADD_FLOAT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_SUBTRACT_FLOAT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_ADD_FLOAT_INPLACE] = 0, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE] = 0, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE] = 0, + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT] = 0, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT] = 0, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT] = 0, + [_BINARY_OP_TRUEDIV_FLOAT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG, + [_BINARY_OP_ADD_UNICODE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_PURE_FLAG, + [_BINARY_OP_INPLACE_ADD_UNICODE] = HAS_LOCAL_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_BINARY_OP_EXTEND_LHS] = HAS_EXIT_FLAG, + [_GUARD_BINARY_OP_EXTEND_RHS] = HAS_EXIT_FLAG, + [_GUARD_BINARY_OP_EXTEND] = HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, + [_BINARY_OP_EXTEND] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_BINARY_SLICE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_STORE_SLICE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_BINARY_OP_SUBSCR_LIST_INT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, - [_BINARY_OP_SUBSCR_LIST_SLICE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_BINARY_OP_SUBSCR_STR_INT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, + [_BINARY_OP_SUBSCR_LIST_INT] = HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, + [_BINARY_OP_SUBSCR_LIST_SLICE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_BINARY_OP_SUBSCR_STR_INT] = HAS_EXIT_FLAG, + [_BINARY_OP_SUBSCR_USTR_INT] = HAS_EXIT_FLAG, [_GUARD_NOS_TUPLE] = HAS_EXIT_FLAG, [_GUARD_TOS_TUPLE] = HAS_EXIT_FLAG, - [_BINARY_OP_SUBSCR_TUPLE_INT] = HAS_DEOPT_FLAG, - [_GUARD_NOS_DICT] = HAS_EXIT_FLAG, + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS] = HAS_EXIT_FLAG, + [_BINARY_OP_SUBSCR_TUPLE_INT] = 0, + [_GUARD_NOS_DICT_SUBSCRIPT] = HAS_DEOPT_FLAG, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT] = HAS_DEOPT_FLAG, + [_GUARD_TOS_ANY_DICT] = HAS_EXIT_FLAG, [_GUARD_TOS_DICT] = HAS_EXIT_FLAG, - [_BINARY_OP_SUBSCR_DICT] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_BINARY_OP_SUBSCR_CHECK_FUNC] = HAS_DEOPT_FLAG, + [_GUARD_TOS_FROZENDICT] = HAS_EXIT_FLAG, + [_BINARY_OP_SUBSCR_DICT_KNOWN_HASH] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_BINARY_OP_SUBSCR_DICT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_BINARY_OP_SUBSCR_CHECK_FUNC] = HAS_EXIT_FLAG, [_BINARY_OP_SUBSCR_INIT_CALL] = 0, [_LIST_APPEND] = HAS_ARG_FLAG | HAS_ERROR_FLAG, [_SET_ADD] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_STORE_SUBSCR] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_STORE_SUBSCR_LIST_INT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, [_STORE_SUBSCR_DICT] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_STORE_SUBSCR_DICT_KNOWN_HASH] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_DELETE_SUBSCR] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_INTRINSIC_1] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_INTRINSIC_2] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_RETURN_VALUE] = HAS_ESCAPES_FLAG, + [_CALL_INTRINSIC_1] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CALL_INTRINSIC_2] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_MAKE_HEAP_SAFE] = 0, + [_RETURN_VALUE] = HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG, [_GET_AITER] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_GET_ANEXT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_GET_AWAITABLE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_SEND_GEN_FRAME] = HAS_ARG_FLAG | HAS_DEOPT_FLAG, - [_YIELD_VALUE] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, + [_SEND_GEN_FRAME] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_GUARD_TOS_IS_NONE] = HAS_EXIT_FLAG, + [_GUARD_NOS_NOT_NULL] = HAS_EXIT_FLAG, + [_SEND_VIRTUAL_TIER_TWO] = HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG, + [_GUARD_3OS_ASYNC_GEN_ASEND] = HAS_EXIT_FLAG, + [_SEND_ASYNC_GEN_TIER_TWO] = HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_YIELD_VALUE] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG, [_POP_EXCEPT] = HAS_ESCAPES_FLAG, [_LOAD_COMMON_CONSTANT] = HAS_ARG_FLAG, [_LOAD_BUILD_CLASS] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_STORE_NAME] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_DELETE_NAME] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_UNPACK_SEQUENCE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_UNPACK_SEQUENCE_TWO_TUPLE] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, - [_UNPACK_SEQUENCE_TUPLE] = HAS_ARG_FLAG | HAS_DEOPT_FLAG, - [_UNPACK_SEQUENCE_LIST] = HAS_ARG_FLAG | HAS_DEOPT_FLAG, + [_UNPACK_SEQUENCE_TWO_TUPLE] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE] = 0, + [_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE] = 0, + [_UNPACK_SEQUENCE_TUPLE] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, + [_UNPACK_SEQUENCE_UNIQUE_TUPLE] = HAS_ARG_FLAG, + [_UNPACK_SEQUENCE_LIST] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, [_UNPACK_EX] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_STORE_ATTR] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_DELETE_ATTR] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_STORE_GLOBAL] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_DELETE_GLOBAL] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, - [_LOAD_LOCALS] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_LOAD_LOCALS] = HAS_ERROR_FLAG, + [_LOAD_FROM_DICT_OR_GLOBALS] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_LOAD_NAME] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_LOAD_GLOBAL] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_PUSH_NULL_CONDITIONAL] = HAS_ARG_FLAG, @@ -151,82 +206,97 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = { [_LOAD_DEREF] = HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_STORE_DEREF] = HAS_ARG_FLAG | HAS_FREE_FLAG | HAS_ESCAPES_FLAG, [_COPY_FREE_VARS] = HAS_ARG_FLAG, - [_BUILD_STRING] = HAS_ARG_FLAG | HAS_ERROR_FLAG, + [_BUILD_STRING] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_BUILD_INTERPOLATION] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_BUILD_TEMPLATE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_BUILD_TUPLE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG, [_BUILD_LIST] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, - [_LIST_EXTEND] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_SET_UPDATE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_LIST_EXTEND] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_SET_UPDATE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_BUILD_SET] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_BUILD_MAP] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_SETUP_ANNOTATIONS] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_DICT_UPDATE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_DICT_MERGE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_DICT_UPDATE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_DICT_MERGE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_MAP_ADD] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_LOAD_SUPER_ATTR_ATTR] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_LOAD_SUPER_ATTR_METHOD] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_LOAD_SUPER_ATTR_ATTR] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_NOS_TYPE_VERSION] = HAS_EXIT_FLAG, + [_GUARD_LOAD_SUPER_ATTR_METHOD] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_LOAD_SUPER_ATTR_METHOD] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_LOAD_ATTR] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_GUARD_TYPE_VERSION] = HAS_EXIT_FLAG, - [_GUARD_TYPE_VERSION_AND_LOCK] = HAS_EXIT_FLAG, - [_CHECK_MANAGED_OBJECT_HAS_VALUES] = HAS_DEOPT_FLAG, - [_LOAD_ATTR_INSTANCE_VALUE] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, - [_LOAD_ATTR_MODULE] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, - [_LOAD_ATTR_WITH_HINT] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, - [_LOAD_ATTR_SLOT] = HAS_DEOPT_FLAG, + [_GUARD_TYPE_VERSION_LOCKED] = HAS_EXIT_FLAG, + [_GUARD_TYPE] = HAS_EXIT_FLAG, + [_CHECK_MANAGED_OBJECT_HAS_VALUES] = HAS_EXIT_FLAG, + [_LOAD_ATTR_INSTANCE_VALUE] = HAS_DEOPT_FLAG, + [_LOAD_ATTR_MODULE] = HAS_EXIT_FLAG, + [_LOAD_ATTR_WITH_HINT] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG, + [_LOAD_ATTR_SLOT] = HAS_EXIT_FLAG, [_CHECK_ATTR_CLASS] = HAS_EXIT_FLAG, - [_LOAD_ATTR_CLASS] = 0, - [_LOAD_ATTR_PROPERTY_FRAME] = HAS_ARG_FLAG | HAS_DEOPT_FLAG, + [_LOAD_ATTR_CLASS] = HAS_ESCAPES_FLAG, + [_LOAD_ATTR_PROPERTY_FRAME] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_EXIT_FLAG, [_GUARD_DORV_NO_DICT] = HAS_EXIT_FLAG, [_STORE_ATTR_INSTANCE_VALUE] = HAS_ESCAPES_FLAG, + [_LOCK_OBJECT] = HAS_DEOPT_FLAG, [_STORE_ATTR_WITH_HINT] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, [_STORE_ATTR_SLOT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, [_COMPARE_OP] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_COMPARE_OP_FLOAT] = HAS_ARG_FLAG, - [_COMPARE_OP_INT] = HAS_ARG_FLAG | HAS_DEOPT_FLAG, + [_COMPARE_OP_INT] = HAS_ARG_FLAG, [_COMPARE_OP_STR] = HAS_ARG_FLAG, [_IS_OP] = HAS_ARG_FLAG, - [_CONTAINS_OP] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_GUARD_TOS_ANY_SET] = HAS_DEOPT_FLAG, - [_CONTAINS_OP_SET] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CONTAINS_OP_DICT] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_CONTAINS_OP] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_TOS_ANY_SET] = HAS_EXIT_FLAG, + [_GUARD_TOS_SET] = HAS_EXIT_FLAG, + [_GUARD_TOS_FROZENSET] = HAS_EXIT_FLAG, + [_CONTAINS_OP_SET] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CONTAINS_OP_DICT] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_CHECK_EG_MATCH] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_CHECK_EXC_MATCH] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_IMPORT_NAME] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_IMPORT_FROM] = HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_IS_NONE] = 0, + [_IS_NONE] = HAS_ESCAPES_FLAG, [_GET_LEN] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_MATCH_CLASS] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_MATCH_CLASS] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_MATCH_MAPPING] = 0, [_MATCH_SEQUENCE] = 0, [_MATCH_KEYS] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_GET_ITER] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_GET_YIELD_FROM_ITER] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GET_ITER] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_ITERATOR] = HAS_EXIT_FLAG, + [_GUARD_ITER_VIRTUAL] = HAS_EXIT_FLAG, + [_PUSH_TAGGED_ZERO] = 0, + [_GET_ITER_TRAD] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_FOR_ITER_TIER_TWO] = HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_TYPE_ITER] = HAS_EXIT_FLAG, + [_ITER_NEXT_INLINE] = HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_NOS_ITER_VIRTUAL] = HAS_EXIT_FLAG, + [_GUARD_TOS_NOT_NULL] = HAS_EXIT_FLAG, + [_FOR_ITER_VIRTUAL_TIER_TWO] = HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_ITER_CHECK_LIST] = HAS_EXIT_FLAG, [_GUARD_NOT_EXHAUSTED_LIST] = HAS_EXIT_FLAG, - [_ITER_NEXT_LIST_TIER_TWO] = HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, + [_ITER_NEXT_LIST_TIER_TWO] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG, [_ITER_CHECK_TUPLE] = HAS_EXIT_FLAG, [_GUARD_NOT_EXHAUSTED_TUPLE] = HAS_EXIT_FLAG, [_ITER_NEXT_TUPLE] = 0, [_ITER_CHECK_RANGE] = HAS_EXIT_FLAG, [_GUARD_NOT_EXHAUSTED_RANGE] = HAS_EXIT_FLAG, [_ITER_NEXT_RANGE] = HAS_ERROR_FLAG, - [_FOR_ITER_GEN_FRAME] = HAS_ARG_FLAG | HAS_DEOPT_FLAG, + [_FOR_ITER_GEN_FRAME] = HAS_ARG_FLAG | HAS_EXIT_FLAG, [_INSERT_NULL] = 0, [_LOAD_SPECIAL] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_WITH_EXCEPT_START] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_PUSH_EXC_INFO] = 0, - [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT] = HAS_DEOPT_FLAG, - [_GUARD_KEYS_VERSION] = HAS_DEOPT_FLAG, + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT] = HAS_EXIT_FLAG, + [_GUARD_KEYS_VERSION] = HAS_EXIT_FLAG, [_LOAD_ATTR_METHOD_WITH_VALUES] = HAS_ARG_FLAG, [_LOAD_ATTR_METHOD_NO_DICT] = HAS_ARG_FLAG, [_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, [_LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, - [_CHECK_ATTR_METHOD_LAZY_DICT] = HAS_DEOPT_FLAG, + [_CHECK_ATTR_METHOD_LAZY_DICT] = HAS_EXIT_FLAG, [_LOAD_ATTR_METHOD_LAZY_DICT] = HAS_ARG_FLAG, [_MAYBE_EXPAND_METHOD] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, - [_PY_FRAME_GENERAL] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_PY_FRAME_GENERAL] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, [_CHECK_FUNCTION_VERSION] = HAS_ARG_FLAG | HAS_EXIT_FLAG, [_CHECK_FUNCTION_VERSION_INLINE] = HAS_EXIT_FLAG, [_CHECK_METHOD_VERSION] = HAS_ARG_FLAG | HAS_EXIT_FLAG, @@ -245,369 +315,5924 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = { [_INIT_CALL_PY_EXACT_ARGS_3] = HAS_PURE_FLAG, [_INIT_CALL_PY_EXACT_ARGS_4] = HAS_PURE_FLAG, [_INIT_CALL_PY_EXACT_ARGS] = HAS_ARG_FLAG | HAS_PURE_FLAG, - [_PUSH_FRAME] = 0, - [_GUARD_NOS_NULL] = HAS_DEOPT_FLAG, - [_GUARD_CALLABLE_TYPE_1] = HAS_DEOPT_FLAG, - [_CALL_TYPE_1] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, - [_GUARD_CALLABLE_STR_1] = HAS_DEOPT_FLAG, - [_CALL_STR_1] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_GUARD_CALLABLE_TUPLE_1] = HAS_DEOPT_FLAG, - [_CALL_TUPLE_1] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CHECK_AND_ALLOCATE_OBJECT] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, - [_CREATE_INIT_FRAME] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_PUSH_FRAME] = HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG, + [_GUARD_NOS_NULL] = HAS_EXIT_FLAG, + [_GUARD_THIRD_NULL] = HAS_EXIT_FLAG, + [_GUARD_CALLABLE_TYPE_1] = HAS_EXIT_FLAG, + [_CALL_TYPE_1] = HAS_ARG_FLAG, + [_GUARD_CALLABLE_STR_1] = HAS_EXIT_FLAG, + [_CALL_STR_1] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_TUPLE_1] = HAS_EXIT_FLAG, + [_CALL_TUPLE_1] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CHECK_OBJECT] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_ALLOCATE_OBJECT] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CREATE_INIT_FRAME] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, [_EXIT_INIT_CHECK] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, - [_CALL_BUILTIN_CLASS] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_BUILTIN_O] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_BUILTIN_FAST] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_BUILTIN_FAST_WITH_KEYWORDS] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_GUARD_CALLABLE_LEN] = HAS_DEOPT_FLAG, + [_GUARD_CALLABLE_BUILTIN_CLASS] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_BUILTIN_CLASS] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_BUILTIN_O] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_BUILTIN_O] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_BUILTIN_FAST] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_BUILTIN_FAST] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_BUILTIN_FAST_WITH_KEYWORDS] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_LEN] = HAS_EXIT_FLAG, [_CALL_LEN] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, - [_CALL_ISINSTANCE] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, - [_CALL_LIST_APPEND] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_METHOD_DESCRIPTOR_O] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_METHOD_DESCRIPTOR_NOARGS] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_CALL_METHOD_DESCRIPTOR_FAST] = HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_ISINSTANCE] = HAS_EXIT_FLAG, + [_CALL_ISINSTANCE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_LIST_APPEND] = HAS_EXIT_FLAG, + [_CALL_LIST_APPEND] = HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_O] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_METHOD_DESCRIPTOR_O] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CHECK_RECURSION_LIMIT] = HAS_EXIT_FLAG, + [_CALL_METHOD_DESCRIPTOR_O_INLINE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_METHOD_DESCRIPTOR_NOARGS] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_CALL_METHOD_DESCRIPTOR_FAST] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_CALL_METHOD_DESCRIPTOR_FAST_INLINE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, [_MAYBE_EXPAND_METHOD_KW] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, - [_PY_FRAME_KW] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_PY_FRAME_KW] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, [_CHECK_FUNCTION_VERSION_KW] = HAS_ARG_FLAG | HAS_EXIT_FLAG, [_CHECK_METHOD_VERSION_KW] = HAS_ARG_FLAG | HAS_EXIT_FLAG, [_EXPAND_METHOD_KW] = HAS_ARG_FLAG | HAS_ESCAPES_FLAG, [_CHECK_IS_NOT_PY_CALLABLE_KW] = HAS_ARG_FLAG | HAS_EXIT_FLAG, [_CALL_KW_NON_PY] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_MAKE_CALLARGS_A_TUPLE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, - [_MAKE_FUNCTION] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_SET_FUNCTION_ATTRIBUTE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_RETURN_GENERATOR] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, - [_BUILD_SLICE] = HAS_ARG_FLAG | HAS_ERROR_FLAG, + [_CHECK_IS_PY_CALLABLE_EX] = HAS_EXIT_FLAG, + [_PY_FRAME_EX] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, + [_CHECK_IS_NOT_PY_CALLABLE_EX] = HAS_EXIT_FLAG, + [_CALL_FUNCTION_EX_NON_PY_GENERAL] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_MAKE_FUNCTION] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_SET_FUNCTION_ATTRIBUTE] = HAS_ARG_FLAG, + [_RETURN_GENERATOR] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG, + [_BUILD_SLICE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_CONVERT_VALUE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_FORMAT_SIMPLE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, [_FORMAT_WITH_SPEC] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG, + [_COPY_1] = HAS_PURE_FLAG, + [_COPY_2] = HAS_PURE_FLAG, + [_COPY_3] = HAS_PURE_FLAG, [_COPY] = HAS_ARG_FLAG | HAS_PURE_FLAG, [_BINARY_OP] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG, + [_SWAP_2] = HAS_PURE_FLAG, + [_SWAP_3] = HAS_PURE_FLAG, [_SWAP] = HAS_ARG_FLAG | HAS_PURE_FLAG, [_GUARD_IS_TRUE_POP] = HAS_EXIT_FLAG, [_GUARD_IS_FALSE_POP] = HAS_EXIT_FLAG, - [_GUARD_IS_NONE_POP] = HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, + [_GUARD_BIT_IS_SET_POP_4] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_SET_POP_5] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_SET_POP_6] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_SET_POP_7] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_SET_POP] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_GUARD_BIT_IS_UNSET_POP_4] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_UNSET_POP_5] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_UNSET_POP_6] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_UNSET_POP_7] = HAS_EXIT_FLAG, + [_GUARD_BIT_IS_UNSET_POP] = HAS_ARG_FLAG | HAS_EXIT_FLAG, + [_GUARD_IS_NONE_POP] = HAS_EXIT_FLAG, [_GUARD_IS_NOT_NONE_POP] = HAS_EXIT_FLAG | HAS_ESCAPES_FLAG, [_JUMP_TO_TOP] = 0, [_SET_IP] = 0, [_CHECK_STACK_SPACE_OPERAND] = HAS_DEOPT_FLAG, [_SAVE_RETURN_OFFSET] = HAS_ARG_FLAG, [_EXIT_TRACE] = HAS_ESCAPES_FLAG, + [_DYNAMIC_EXIT] = HAS_ESCAPES_FLAG, [_CHECK_VALIDITY] = HAS_DEOPT_FLAG, [_LOAD_CONST_INLINE] = HAS_PURE_FLAG, - [_POP_TOP_LOAD_CONST_INLINE] = HAS_ESCAPES_FLAG | HAS_PURE_FLAG, [_LOAD_CONST_INLINE_BORROW] = HAS_PURE_FLAG, - [_POP_TOP_LOAD_CONST_INLINE_BORROW] = HAS_ESCAPES_FLAG | HAS_PURE_FLAG, - [_POP_TWO_LOAD_CONST_INLINE_BORROW] = HAS_ESCAPES_FLAG | HAS_PURE_FLAG, - [_CHECK_FUNCTION] = HAS_DEOPT_FLAG, - [_START_EXECUTOR] = 0, + [_RROT_3] = HAS_PURE_FLAG, + [_START_EXECUTOR] = HAS_DEOPT_FLAG, [_MAKE_WARM] = 0, [_FATAL_ERROR] = 0, - [_DEOPT] = 0, - [_ERROR_POP_N] = HAS_ARG_FLAG, - [_TIER2_RESUME_CHECK] = HAS_DEOPT_FLAG, + [_DEOPT] = HAS_SYNC_SP_FLAG, + [_HANDLE_PENDING_AND_DEOPT] = HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, + [_ERROR_POP_N] = HAS_ARG_FLAG | HAS_SYNC_SP_FLAG, + [_SPILL_OR_RELOAD] = 0, + [_TIER2_RESUME_CHECK] = HAS_PERIODIC_FLAG, + [_COLD_EXIT] = HAS_ESCAPES_FLAG | HAS_SYNC_SP_FLAG, + [_COLD_DYNAMIC_EXIT] = HAS_SYNC_SP_FLAG, + [_GUARD_CODE_VERSION__PUSH_FRAME] = HAS_EXIT_FLAG, + [_GUARD_CODE_VERSION_YIELD_VALUE] = HAS_EXIT_FLAG, + [_GUARD_CODE_VERSION_RETURN_VALUE] = HAS_EXIT_FLAG, + [_GUARD_CODE_VERSION_RETURN_GENERATOR] = HAS_EXIT_FLAG, + [_GUARD_IP__PUSH_FRAME] = HAS_EXIT_FLAG, + [_GUARD_IP_YIELD_VALUE] = HAS_EXIT_FLAG, + [_GUARD_IP_RETURN_VALUE] = HAS_EXIT_FLAG, + [_GUARD_IP_RETURN_GENERATOR] = HAS_EXIT_FLAG, + [_RECORD_TOS] = HAS_RECORDS_VALUE_FLAG, + [_RECORD_TOS_TYPE] = HAS_RECORDS_VALUE_FLAG, + [_RECORD_NOS] = HAS_RECORDS_VALUE_FLAG, + [_RECORD_NOS_TYPE] = HAS_RECORDS_VALUE_FLAG, + [_RECORD_NOS_GEN_FUNC] = HAS_RECORDS_VALUE_FLAG, + [_RECORD_3OS_GEN_FUNC] = HAS_RECORDS_VALUE_FLAG, + [_RECORD_4OS] = HAS_RECORDS_VALUE_FLAG, + [_RECORD_CALLABLE] = HAS_ARG_FLAG | HAS_RECORDS_VALUE_FLAG, + [_RECORD_CALLABLE_KW] = HAS_ARG_FLAG | HAS_RECORDS_VALUE_FLAG, + [_RECORD_BOUND_METHOD] = HAS_ARG_FLAG | HAS_RECORDS_VALUE_FLAG, + [_RECORD_CODE] = HAS_RECORDS_VALUE_FLAG, +}; + +const ReplicationRange _PyUop_Replication[MAX_UOP_ID+1] = { + [_LOAD_FAST] = { 0, 8 }, + [_LOAD_FAST_BORROW] = { 0, 8 }, + [_LOAD_SMALL_INT] = { 0, 4 }, + [_SWAP_FAST] = { 0, 8 }, + [_INIT_CALL_PY_EXACT_ARGS] = { 0, 5 }, + [_COPY] = { 1, 4 }, + [_SWAP] = { 2, 4 }, + [_GUARD_BIT_IS_SET_POP] = { 4, 8 }, + [_GUARD_BIT_IS_UNSET_POP] = { 4, 8 }, +}; + +const _PyUopCachingInfo _PyUop_Caching[MAX_UOP_ID+1] = { + [_NOP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _NOP_r00 }, + { 1, 1, _NOP_r11 }, + { 2, 2, _NOP_r22 }, + { 3, 3, _NOP_r33 }, + }, + }, + [_CHECK_PERIODIC] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_PERIODIC_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_PERIODIC_IF_NOT_YIELD_FROM] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_PERIODIC_IF_NOT_YIELD_FROM_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_RESUME_CHECK] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _RESUME_CHECK_r00 }, + { 1, 1, _RESUME_CHECK_r11 }, + { 2, 2, _RESUME_CHECK_r22 }, + { 3, 3, _RESUME_CHECK_r33 }, + }, + }, + [_LOAD_FAST_CHECK] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_CHECK_r01 }, + { 2, 1, _LOAD_FAST_CHECK_r12 }, + { 3, 2, _LOAD_FAST_CHECK_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_0] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_0_r01 }, + { 2, 1, _LOAD_FAST_0_r12 }, + { 3, 2, _LOAD_FAST_0_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_1] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_1_r01 }, + { 2, 1, _LOAD_FAST_1_r12 }, + { 3, 2, _LOAD_FAST_1_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_2] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_2_r01 }, + { 2, 1, _LOAD_FAST_2_r12 }, + { 3, 2, _LOAD_FAST_2_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_3] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_3_r01 }, + { 2, 1, _LOAD_FAST_3_r12 }, + { 3, 2, _LOAD_FAST_3_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_4] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_4_r01 }, + { 2, 1, _LOAD_FAST_4_r12 }, + { 3, 2, _LOAD_FAST_4_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_5] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_5_r01 }, + { 2, 1, _LOAD_FAST_5_r12 }, + { 3, 2, _LOAD_FAST_5_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_6] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_6_r01 }, + { 2, 1, _LOAD_FAST_6_r12 }, + { 3, 2, _LOAD_FAST_6_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_7] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_7_r01 }, + { 2, 1, _LOAD_FAST_7_r12 }, + { 3, 2, _LOAD_FAST_7_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_r01 }, + { 2, 1, _LOAD_FAST_r12 }, + { 3, 2, _LOAD_FAST_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_0] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_0_r01 }, + { 2, 1, _LOAD_FAST_BORROW_0_r12 }, + { 3, 2, _LOAD_FAST_BORROW_0_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_1] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_1_r01 }, + { 2, 1, _LOAD_FAST_BORROW_1_r12 }, + { 3, 2, _LOAD_FAST_BORROW_1_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_2] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_2_r01 }, + { 2, 1, _LOAD_FAST_BORROW_2_r12 }, + { 3, 2, _LOAD_FAST_BORROW_2_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_3] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_3_r01 }, + { 2, 1, _LOAD_FAST_BORROW_3_r12 }, + { 3, 2, _LOAD_FAST_BORROW_3_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_4] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_4_r01 }, + { 2, 1, _LOAD_FAST_BORROW_4_r12 }, + { 3, 2, _LOAD_FAST_BORROW_4_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_5] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_5_r01 }, + { 2, 1, _LOAD_FAST_BORROW_5_r12 }, + { 3, 2, _LOAD_FAST_BORROW_5_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_6] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_6_r01 }, + { 2, 1, _LOAD_FAST_BORROW_6_r12 }, + { 3, 2, _LOAD_FAST_BORROW_6_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW_7] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_7_r01 }, + { 2, 1, _LOAD_FAST_BORROW_7_r12 }, + { 3, 2, _LOAD_FAST_BORROW_7_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_BORROW] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_BORROW_r01 }, + { 2, 1, _LOAD_FAST_BORROW_r12 }, + { 3, 2, _LOAD_FAST_BORROW_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FAST_AND_CLEAR] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_FAST_AND_CLEAR_r01 }, + { 2, 1, _LOAD_FAST_AND_CLEAR_r12 }, + { 3, 2, _LOAD_FAST_AND_CLEAR_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_CONST] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_CONST_r01 }, + { 2, 1, _LOAD_CONST_r12 }, + { 3, 2, _LOAD_CONST_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_SMALL_INT_0] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_SMALL_INT_0_r01 }, + { 2, 1, _LOAD_SMALL_INT_0_r12 }, + { 3, 2, _LOAD_SMALL_INT_0_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_SMALL_INT_1] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_SMALL_INT_1_r01 }, + { 2, 1, _LOAD_SMALL_INT_1_r12 }, + { 3, 2, _LOAD_SMALL_INT_1_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_SMALL_INT_2] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_SMALL_INT_2_r01 }, + { 2, 1, _LOAD_SMALL_INT_2_r12 }, + { 3, 2, _LOAD_SMALL_INT_2_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_SMALL_INT_3] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_SMALL_INT_3_r01 }, + { 2, 1, _LOAD_SMALL_INT_3_r12 }, + { 3, 2, _LOAD_SMALL_INT_3_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_SMALL_INT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_SMALL_INT_r01 }, + { 2, 1, _LOAD_SMALL_INT_r12 }, + { 3, 2, _LOAD_SMALL_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_SWAP_FAST_0] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_0_r01 }, + { 1, 1, _SWAP_FAST_0_r11 }, + { 2, 2, _SWAP_FAST_0_r22 }, + { 3, 3, _SWAP_FAST_0_r33 }, + }, + }, + [_SWAP_FAST_1] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_1_r01 }, + { 1, 1, _SWAP_FAST_1_r11 }, + { 2, 2, _SWAP_FAST_1_r22 }, + { 3, 3, _SWAP_FAST_1_r33 }, + }, + }, + [_SWAP_FAST_2] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_2_r01 }, + { 1, 1, _SWAP_FAST_2_r11 }, + { 2, 2, _SWAP_FAST_2_r22 }, + { 3, 3, _SWAP_FAST_2_r33 }, + }, + }, + [_SWAP_FAST_3] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_3_r01 }, + { 1, 1, _SWAP_FAST_3_r11 }, + { 2, 2, _SWAP_FAST_3_r22 }, + { 3, 3, _SWAP_FAST_3_r33 }, + }, + }, + [_SWAP_FAST_4] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_4_r01 }, + { 1, 1, _SWAP_FAST_4_r11 }, + { 2, 2, _SWAP_FAST_4_r22 }, + { 3, 3, _SWAP_FAST_4_r33 }, + }, + }, + [_SWAP_FAST_5] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_5_r01 }, + { 1, 1, _SWAP_FAST_5_r11 }, + { 2, 2, _SWAP_FAST_5_r22 }, + { 3, 3, _SWAP_FAST_5_r33 }, + }, + }, + [_SWAP_FAST_6] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_6_r01 }, + { 1, 1, _SWAP_FAST_6_r11 }, + { 2, 2, _SWAP_FAST_6_r22 }, + { 3, 3, _SWAP_FAST_6_r33 }, + }, + }, + [_SWAP_FAST_7] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_7_r01 }, + { 1, 1, _SWAP_FAST_7_r11 }, + { 2, 2, _SWAP_FAST_7_r22 }, + { 3, 3, _SWAP_FAST_7_r33 }, + }, + }, + [_SWAP_FAST] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SWAP_FAST_r01 }, + { 1, 1, _SWAP_FAST_r11 }, + { 2, 2, _SWAP_FAST_r22 }, + { 3, 3, _SWAP_FAST_r33 }, + }, + }, + [_POP_TOP] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _POP_TOP_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_POP_TOP_NOP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _POP_TOP_NOP_r00 }, + { 0, 1, _POP_TOP_NOP_r10 }, + { 1, 2, _POP_TOP_NOP_r21 }, + { 2, 3, _POP_TOP_NOP_r32 }, + }, + }, + [_POP_TOP_INT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _POP_TOP_INT_r00 }, + { 0, 1, _POP_TOP_INT_r10 }, + { 1, 2, _POP_TOP_INT_r21 }, + { 2, 3, _POP_TOP_INT_r32 }, + }, + }, + [_POP_TOP_FLOAT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _POP_TOP_FLOAT_r00 }, + { 0, 1, _POP_TOP_FLOAT_r10 }, + { 1, 2, _POP_TOP_FLOAT_r21 }, + { 2, 3, _POP_TOP_FLOAT_r32 }, + }, + }, + [_POP_TOP_UNICODE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _POP_TOP_UNICODE_r00 }, + { 0, 1, _POP_TOP_UNICODE_r10 }, + { 1, 2, _POP_TOP_UNICODE_r21 }, + { 2, 3, _POP_TOP_UNICODE_r32 }, + }, + }, + [_POP_TOP_OPARG] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _POP_TOP_OPARG_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_PUSH_NULL] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _PUSH_NULL_r01 }, + { 2, 1, _PUSH_NULL_r12 }, + { 3, 2, _PUSH_NULL_r23 }, + { -1, -1, -1 }, + }, + }, + [_END_FOR] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _END_FOR_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_POP_ITER] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 0, 2, _POP_ITER_r20 }, + { -1, -1, -1 }, + }, + }, + [_END_SEND] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 3, _END_SEND_r31 }, + }, + }, + [_UNARY_NEGATIVE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _UNARY_NEGATIVE_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNARY_NEGATIVE_FLOAT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _UNARY_NEGATIVE_FLOAT_INPLACE_r02 }, + { 2, 1, _UNARY_NEGATIVE_FLOAT_INPLACE_r12 }, + { 3, 2, _UNARY_NEGATIVE_FLOAT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_UNARY_NOT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _UNARY_NOT_r01 }, + { 1, 1, _UNARY_NOT_r11 }, + { 2, 2, _UNARY_NOT_r22 }, + { 3, 3, _UNARY_NOT_r33 }, + }, + }, + [_TO_BOOL] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _TO_BOOL_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_TO_BOOL_BOOL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _TO_BOOL_BOOL_r01 }, + { 1, 1, _TO_BOOL_BOOL_r11 }, + { 2, 2, _TO_BOOL_BOOL_r22 }, + { 3, 3, _TO_BOOL_BOOL_r33 }, + }, + }, + [_TO_BOOL_INT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _TO_BOOL_INT_r02 }, + { 2, 1, _TO_BOOL_INT_r12 }, + { 3, 2, _TO_BOOL_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_NOS_LIST] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_LIST_r02 }, + { 2, 1, _GUARD_NOS_LIST_r12 }, + { 2, 2, _GUARD_NOS_LIST_r22 }, + { 3, 3, _GUARD_NOS_LIST_r33 }, + }, + }, + [_GUARD_TOS_LIST] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_LIST_r01 }, + { 1, 1, _GUARD_TOS_LIST_r11 }, + { 2, 2, _GUARD_TOS_LIST_r22 }, + { 3, 3, _GUARD_TOS_LIST_r33 }, + }, + }, + [_GUARD_TOS_SLICE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_SLICE_r01 }, + { 1, 1, _GUARD_TOS_SLICE_r11 }, + { 2, 2, _GUARD_TOS_SLICE_r22 }, + { 3, 3, _GUARD_TOS_SLICE_r33 }, + }, + }, + [_TO_BOOL_LIST] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _TO_BOOL_LIST_r02 }, + { 2, 1, _TO_BOOL_LIST_r12 }, + { 3, 2, _TO_BOOL_LIST_r23 }, + { -1, -1, -1 }, + }, + }, + [_TO_BOOL_NONE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _TO_BOOL_NONE_r01 }, + { 1, 1, _TO_BOOL_NONE_r11 }, + { 2, 2, _TO_BOOL_NONE_r22 }, + { 3, 3, _TO_BOOL_NONE_r33 }, + }, + }, + [_GUARD_NOS_COMPACT_ASCII] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_COMPACT_ASCII_r02 }, + { 2, 1, _GUARD_NOS_COMPACT_ASCII_r12 }, + { 2, 2, _GUARD_NOS_COMPACT_ASCII_r22 }, + { 3, 3, _GUARD_NOS_COMPACT_ASCII_r33 }, + }, + }, + [_GUARD_NOS_UNICODE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_UNICODE_r02 }, + { 2, 1, _GUARD_NOS_UNICODE_r12 }, + { 2, 2, _GUARD_NOS_UNICODE_r22 }, + { 3, 3, _GUARD_NOS_UNICODE_r33 }, + }, + }, + [_GUARD_TOS_UNICODE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_UNICODE_r01 }, + { 1, 1, _GUARD_TOS_UNICODE_r11 }, + { 2, 2, _GUARD_TOS_UNICODE_r22 }, + { 3, 3, _GUARD_TOS_UNICODE_r33 }, + }, + }, + [_TO_BOOL_STR] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _TO_BOOL_STR_r02 }, + { 2, 1, _TO_BOOL_STR_r12 }, + { 3, 2, _TO_BOOL_STR_r23 }, + { -1, -1, -1 }, + }, + }, + [_REPLACE_WITH_TRUE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _REPLACE_WITH_TRUE_r02 }, + { 2, 1, _REPLACE_WITH_TRUE_r12 }, + { 3, 2, _REPLACE_WITH_TRUE_r23 }, + { -1, -1, -1 }, + }, + }, + [_UNARY_INVERT] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _UNARY_INVERT_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_NOS_INT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_INT_r02 }, + { 2, 1, _GUARD_NOS_INT_r12 }, + { 2, 2, _GUARD_NOS_INT_r22 }, + { 3, 3, _GUARD_NOS_INT_r33 }, + }, + }, + [_GUARD_TOS_INT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_INT_r01 }, + { 1, 1, _GUARD_TOS_INT_r11 }, + { 2, 2, _GUARD_TOS_INT_r22 }, + { 3, 3, _GUARD_TOS_INT_r33 }, + }, + }, + [_GUARD_NOS_OVERFLOWED] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_OVERFLOWED_r02 }, + { 2, 1, _GUARD_NOS_OVERFLOWED_r12 }, + { 2, 2, _GUARD_NOS_OVERFLOWED_r22 }, + { 3, 3, _GUARD_NOS_OVERFLOWED_r33 }, + }, + }, + [_GUARD_TOS_OVERFLOWED] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_OVERFLOWED_r01 }, + { 1, 1, _GUARD_TOS_OVERFLOWED_r11 }, + { 2, 2, _GUARD_TOS_OVERFLOWED_r22 }, + { 3, 3, _GUARD_TOS_OVERFLOWED_r33 }, + }, + }, + [_BINARY_OP_MULTIPLY_INT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_MULTIPLY_INT_r03 }, + { 3, 1, _BINARY_OP_MULTIPLY_INT_r13 }, + { 3, 2, _BINARY_OP_MULTIPLY_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_ADD_INT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_ADD_INT_r03 }, + { 3, 1, _BINARY_OP_ADD_INT_r13 }, + { 3, 2, _BINARY_OP_ADD_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBTRACT_INT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_SUBTRACT_INT_r03 }, + { 3, 1, _BINARY_OP_SUBTRACT_INT_r13 }, + { 3, 2, _BINARY_OP_SUBTRACT_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_ADD_INT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_ADD_INT_INPLACE_r03 }, + { 3, 1, _BINARY_OP_ADD_INT_INPLACE_r13 }, + { 3, 2, _BINARY_OP_ADD_INT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBTRACT_INT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_SUBTRACT_INT_INPLACE_r03 }, + { 3, 1, _BINARY_OP_SUBTRACT_INT_INPLACE_r13 }, + { 3, 2, _BINARY_OP_SUBTRACT_INT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_MULTIPLY_INT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_MULTIPLY_INT_INPLACE_r03 }, + { 3, 1, _BINARY_OP_MULTIPLY_INT_INPLACE_r13 }, + { 3, 2, _BINARY_OP_MULTIPLY_INT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_ADD_INT_INPLACE_RIGHT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_ADD_INT_INPLACE_RIGHT_r03 }, + { 3, 1, _BINARY_OP_ADD_INT_INPLACE_RIGHT_r13 }, + { 3, 2, _BINARY_OP_ADD_INT_INPLACE_RIGHT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r03 }, + { 3, 1, _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r13 }, + { 3, 2, _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r03 }, + { 3, 1, _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r13 }, + { 3, 2, _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_NOS_FLOAT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_FLOAT_r02 }, + { 2, 1, _GUARD_NOS_FLOAT_r12 }, + { 2, 2, _GUARD_NOS_FLOAT_r22 }, + { 3, 3, _GUARD_NOS_FLOAT_r33 }, + }, + }, + [_GUARD_TOS_FLOAT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_FLOAT_r01 }, + { 1, 1, _GUARD_TOS_FLOAT_r11 }, + { 2, 2, _GUARD_TOS_FLOAT_r22 }, + { 3, 3, _GUARD_TOS_FLOAT_r33 }, + }, + }, + [_BINARY_OP_MULTIPLY_FLOAT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_MULTIPLY_FLOAT_r03 }, + { 3, 1, _BINARY_OP_MULTIPLY_FLOAT_r13 }, + { 3, 2, _BINARY_OP_MULTIPLY_FLOAT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_ADD_FLOAT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_ADD_FLOAT_r03 }, + { 3, 1, _BINARY_OP_ADD_FLOAT_r13 }, + { 3, 2, _BINARY_OP_ADD_FLOAT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBTRACT_FLOAT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_SUBTRACT_FLOAT_r03 }, + { 3, 1, _BINARY_OP_SUBTRACT_FLOAT_r13 }, + { 3, 2, _BINARY_OP_SUBTRACT_FLOAT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_ADD_FLOAT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_ADD_FLOAT_INPLACE_r03 }, + { 3, 1, _BINARY_OP_ADD_FLOAT_INPLACE_r13 }, + { 3, 2, _BINARY_OP_ADD_FLOAT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_SUBTRACT_FLOAT_INPLACE_r03 }, + { 3, 1, _BINARY_OP_SUBTRACT_FLOAT_INPLACE_r13 }, + { 3, 2, _BINARY_OP_SUBTRACT_FLOAT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_MULTIPLY_FLOAT_INPLACE_r03 }, + { 3, 1, _BINARY_OP_MULTIPLY_FLOAT_INPLACE_r13 }, + { 3, 2, _BINARY_OP_MULTIPLY_FLOAT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r03 }, + { 3, 1, _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r13 }, + { 3, 2, _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r03 }, + { 3, 1, _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r13 }, + { 3, 2, _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r03 }, + { 3, 1, _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r13 }, + { 3, 2, _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_TRUEDIV_FLOAT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_TRUEDIV_FLOAT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_TRUEDIV_FLOAT_INPLACE_r03 }, + { 3, 1, _BINARY_OP_TRUEDIV_FLOAT_INPLACE_r13 }, + { 3, 2, _BINARY_OP_TRUEDIV_FLOAT_INPLACE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r03 }, + { 3, 1, _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r13 }, + { 3, 2, _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_ADD_UNICODE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_ADD_UNICODE_r03 }, + { 3, 1, _BINARY_OP_ADD_UNICODE_r13 }, + { 3, 2, _BINARY_OP_ADD_UNICODE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_INPLACE_ADD_UNICODE] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _BINARY_OP_INPLACE_ADD_UNICODE_r21 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_BINARY_OP_EXTEND_LHS] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_BINARY_OP_EXTEND_LHS_r02 }, + { 2, 1, _GUARD_BINARY_OP_EXTEND_LHS_r12 }, + { 2, 2, _GUARD_BINARY_OP_EXTEND_LHS_r22 }, + { 3, 3, _GUARD_BINARY_OP_EXTEND_LHS_r33 }, + }, + }, + [_GUARD_BINARY_OP_EXTEND_RHS] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_BINARY_OP_EXTEND_RHS_r02 }, + { 2, 1, _GUARD_BINARY_OP_EXTEND_RHS_r12 }, + { 2, 2, _GUARD_BINARY_OP_EXTEND_RHS_r22 }, + { 3, 3, _GUARD_BINARY_OP_EXTEND_RHS_r33 }, + }, + }, + [_GUARD_BINARY_OP_EXTEND] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 2, 2, _GUARD_BINARY_OP_EXTEND_r22 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_EXTEND] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_EXTEND_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_SLICE] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 3, _BINARY_SLICE_r31 }, + }, + }, + [_STORE_SLICE] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 0, 3, _STORE_SLICE_r30 }, + }, + }, + [_BINARY_OP_SUBSCR_LIST_INT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_SUBSCR_LIST_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBSCR_LIST_SLICE] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_SUBSCR_LIST_SLICE_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBSCR_STR_INT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_SUBSCR_STR_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBSCR_USTR_INT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_SUBSCR_USTR_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_NOS_TUPLE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_TUPLE_r02 }, + { 2, 1, _GUARD_NOS_TUPLE_r12 }, + { 2, 2, _GUARD_NOS_TUPLE_r22 }, + { 3, 3, _GUARD_NOS_TUPLE_r33 }, + }, + }, + [_GUARD_TOS_TUPLE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_TUPLE_r01 }, + { 1, 1, _GUARD_TOS_TUPLE_r11 }, + { 2, 2, _GUARD_TOS_TUPLE_r22 }, + { 3, 3, _GUARD_TOS_TUPLE_r33 }, + }, + }, + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r02 }, + { 2, 1, _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r12 }, + { 2, 2, _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r22 }, + { 3, 3, _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r33 }, + }, + }, + [_BINARY_OP_SUBSCR_TUPLE_INT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _BINARY_OP_SUBSCR_TUPLE_INT_r03 }, + { 3, 1, _BINARY_OP_SUBSCR_TUPLE_INT_r13 }, + { 3, 2, _BINARY_OP_SUBSCR_TUPLE_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_NOS_DICT_SUBSCRIPT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_DICT_SUBSCRIPT_r02 }, + { 2, 1, _GUARD_NOS_DICT_SUBSCRIPT_r12 }, + { 2, 2, _GUARD_NOS_DICT_SUBSCRIPT_r22 }, + { 3, 3, _GUARD_NOS_DICT_SUBSCRIPT_r33 }, + }, + }, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r03 }, + { 3, 1, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r13 }, + { 3, 2, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r23 }, + { 3, 3, _GUARD_NOS_DICT_STORE_SUBSCRIPT_r33 }, + }, + }, + [_GUARD_TOS_ANY_DICT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_ANY_DICT_r01 }, + { 1, 1, _GUARD_TOS_ANY_DICT_r11 }, + { 2, 2, _GUARD_TOS_ANY_DICT_r22 }, + { 3, 3, _GUARD_TOS_ANY_DICT_r33 }, + }, + }, + [_GUARD_TOS_DICT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_DICT_r01 }, + { 1, 1, _GUARD_TOS_DICT_r11 }, + { 2, 2, _GUARD_TOS_DICT_r22 }, + { 3, 3, _GUARD_TOS_DICT_r33 }, + }, + }, + [_GUARD_TOS_FROZENDICT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_FROZENDICT_r01 }, + { 1, 1, _GUARD_TOS_FROZENDICT_r11 }, + { 2, 2, _GUARD_TOS_FROZENDICT_r22 }, + { 3, 3, _GUARD_TOS_FROZENDICT_r33 }, + }, + }, + [_BINARY_OP_SUBSCR_DICT_KNOWN_HASH] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_SUBSCR_DICT_KNOWN_HASH_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBSCR_DICT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_SUBSCR_DICT_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBSCR_CHECK_FUNC] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_SUBSCR_CHECK_FUNC_r23 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP_SUBSCR_INIT_CALL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _BINARY_OP_SUBSCR_INIT_CALL_r01 }, + { 1, 1, _BINARY_OP_SUBSCR_INIT_CALL_r11 }, + { 1, 2, _BINARY_OP_SUBSCR_INIT_CALL_r21 }, + { 1, 3, _BINARY_OP_SUBSCR_INIT_CALL_r31 }, + }, + }, + [_LIST_APPEND] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _LIST_APPEND_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_SET_ADD] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _SET_ADD_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_STORE_SUBSCR] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 0, 3, _STORE_SUBSCR_r30 }, + }, + }, + [_STORE_SUBSCR_LIST_INT] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 2, 3, _STORE_SUBSCR_LIST_INT_r32 }, + }, + }, + [_STORE_SUBSCR_DICT] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 3, _STORE_SUBSCR_DICT_r31 }, + }, + }, + [_STORE_SUBSCR_DICT_KNOWN_HASH] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 3, _STORE_SUBSCR_DICT_KNOWN_HASH_r31 }, + }, + }, + [_DELETE_SUBSCR] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 0, 2, _DELETE_SUBSCR_r20 }, + { -1, -1, -1 }, + }, + }, + [_CALL_INTRINSIC_1] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _CALL_INTRINSIC_1_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_INTRINSIC_2] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _CALL_INTRINSIC_2_r23 }, + { -1, -1, -1 }, + }, + }, + [_MAKE_HEAP_SAFE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _MAKE_HEAP_SAFE_r01 }, + { 1, 1, _MAKE_HEAP_SAFE_r11 }, + { 2, 2, _MAKE_HEAP_SAFE_r22 }, + { 3, 3, _MAKE_HEAP_SAFE_r33 }, + }, + }, + [_RETURN_VALUE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _RETURN_VALUE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GET_AITER] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _GET_AITER_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GET_ANEXT] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _GET_ANEXT_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GET_AWAITABLE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _GET_AWAITABLE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_SEND_GEN_FRAME] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 3, _SEND_GEN_FRAME_r33 }, + }, + }, + [_GUARD_TOS_IS_NONE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_IS_NONE_r01 }, + { 1, 1, _GUARD_TOS_IS_NONE_r11 }, + { 2, 2, _GUARD_TOS_IS_NONE_r22 }, + { 3, 3, _GUARD_TOS_IS_NONE_r33 }, + }, + }, + [_GUARD_NOS_NOT_NULL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_NOT_NULL_r02 }, + { 2, 1, _GUARD_NOS_NOT_NULL_r12 }, + { 2, 2, _GUARD_NOS_NOT_NULL_r22 }, + { 3, 3, _GUARD_NOS_NOT_NULL_r33 }, + }, + }, + [_SEND_VIRTUAL_TIER_TWO] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _SEND_VIRTUAL_TIER_TWO_r03 }, + { 3, 1, _SEND_VIRTUAL_TIER_TWO_r13 }, + { 3, 2, _SEND_VIRTUAL_TIER_TWO_r23 }, + { 3, 3, _SEND_VIRTUAL_TIER_TWO_r33 }, + }, + }, + [_GUARD_3OS_ASYNC_GEN_ASEND] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_3OS_ASYNC_GEN_ASEND_r03 }, + { 3, 1, _GUARD_3OS_ASYNC_GEN_ASEND_r13 }, + { 3, 2, _GUARD_3OS_ASYNC_GEN_ASEND_r23 }, + { 3, 3, _GUARD_3OS_ASYNC_GEN_ASEND_r33 }, + }, + }, + [_SEND_ASYNC_GEN_TIER_TWO] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 3, _SEND_ASYNC_GEN_TIER_TWO_r33 }, + }, + }, + [_YIELD_VALUE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _YIELD_VALUE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_POP_EXCEPT] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _POP_EXCEPT_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_COMMON_CONSTANT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_COMMON_CONSTANT_r01 }, + { 2, 1, _LOAD_COMMON_CONSTANT_r12 }, + { 3, 2, _LOAD_COMMON_CONSTANT_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_BUILD_CLASS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _LOAD_BUILD_CLASS_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_STORE_NAME] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _STORE_NAME_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_DELETE_NAME] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _DELETE_NAME_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_SEQUENCE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _UNPACK_SEQUENCE_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_SEQUENCE_TWO_TUPLE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _UNPACK_SEQUENCE_TWO_TUPLE_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r02 }, + { 2, 1, _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r12 }, + { 3, 2, _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r23 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE] = { + .best = { 0, 1, 1, 1 }, + .entries = { + { 3, 0, _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r03 }, + { 3, 1, _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r13 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_SEQUENCE_TUPLE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _UNPACK_SEQUENCE_TUPLE_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_SEQUENCE_UNIQUE_TUPLE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _UNPACK_SEQUENCE_UNIQUE_TUPLE_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_SEQUENCE_LIST] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _UNPACK_SEQUENCE_LIST_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_UNPACK_EX] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _UNPACK_EX_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_STORE_ATTR] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 0, 2, _STORE_ATTR_r20 }, + { -1, -1, -1 }, + }, + }, + [_DELETE_ATTR] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _DELETE_ATTR_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_STORE_GLOBAL] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _STORE_GLOBAL_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_DELETE_GLOBAL] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _DELETE_GLOBAL_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_LOCALS] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_LOCALS_r01 }, + { 2, 1, _LOAD_LOCALS_r12 }, + { 3, 2, _LOAD_LOCALS_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FROM_DICT_OR_GLOBALS] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _LOAD_FROM_DICT_OR_GLOBALS_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_NAME] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _LOAD_NAME_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_GLOBAL] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _LOAD_GLOBAL_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_PUSH_NULL_CONDITIONAL] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _PUSH_NULL_CONDITIONAL_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_GLOBALS_VERSION] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_GLOBALS_VERSION_r00 }, + { 1, 1, _GUARD_GLOBALS_VERSION_r11 }, + { 2, 2, _GUARD_GLOBALS_VERSION_r22 }, + { 3, 3, _GUARD_GLOBALS_VERSION_r33 }, + }, + }, + [_LOAD_GLOBAL_MODULE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _LOAD_GLOBAL_MODULE_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_GLOBAL_BUILTINS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _LOAD_GLOBAL_BUILTINS_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_DELETE_FAST] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _DELETE_FAST_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_MAKE_CELL] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _MAKE_CELL_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_DELETE_DEREF] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _DELETE_DEREF_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_FROM_DICT_OR_DEREF] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _LOAD_FROM_DICT_OR_DEREF_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_DEREF] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _LOAD_DEREF_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_STORE_DEREF] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _STORE_DEREF_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_COPY_FREE_VARS] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _COPY_FREE_VARS_r00 }, + { 1, 1, _COPY_FREE_VARS_r11 }, + { 2, 2, _COPY_FREE_VARS_r22 }, + { 3, 3, _COPY_FREE_VARS_r33 }, + }, + }, + [_BUILD_STRING] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _BUILD_STRING_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_BUILD_INTERPOLATION] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _BUILD_INTERPOLATION_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_BUILD_TEMPLATE] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _BUILD_TEMPLATE_r21 }, + { -1, -1, -1 }, + }, + }, + [_BUILD_TUPLE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _BUILD_TUPLE_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_BUILD_LIST] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _BUILD_LIST_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LIST_EXTEND] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _LIST_EXTEND_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_SET_UPDATE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _SET_UPDATE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_BUILD_SET] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _BUILD_SET_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_BUILD_MAP] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _BUILD_MAP_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_SETUP_ANNOTATIONS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _SETUP_ANNOTATIONS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_DICT_UPDATE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _DICT_UPDATE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_DICT_MERGE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _DICT_MERGE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_MAP_ADD] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 0, 2, _MAP_ADD_r20 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_SUPER_ATTR_ATTR] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 3, _LOAD_SUPER_ATTR_ATTR_r31 }, + }, + }, + [_GUARD_NOS_TYPE_VERSION] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_TYPE_VERSION_r02 }, + { 2, 1, _GUARD_NOS_TYPE_VERSION_r12 }, + { 2, 2, _GUARD_NOS_TYPE_VERSION_r22 }, + { 3, 3, _GUARD_NOS_TYPE_VERSION_r33 }, + }, + }, + [_GUARD_LOAD_SUPER_ATTR_METHOD] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_LOAD_SUPER_ATTR_METHOD_r03 }, + { 3, 1, _GUARD_LOAD_SUPER_ATTR_METHOD_r13 }, + { 3, 2, _GUARD_LOAD_SUPER_ATTR_METHOD_r23 }, + { 3, 3, _GUARD_LOAD_SUPER_ATTR_METHOD_r33 }, + }, + }, + [_LOAD_SUPER_ATTR_METHOD] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 2, 3, _LOAD_SUPER_ATTR_METHOD_r32 }, + }, + }, + [_LOAD_ATTR] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _LOAD_ATTR_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_TYPE_VERSION] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TYPE_VERSION_r01 }, + { 1, 1, _GUARD_TYPE_VERSION_r11 }, + { 2, 2, _GUARD_TYPE_VERSION_r22 }, + { 3, 3, _GUARD_TYPE_VERSION_r33 }, + }, + }, + [_GUARD_TYPE_VERSION_LOCKED] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TYPE_VERSION_LOCKED_r01 }, + { 1, 1, _GUARD_TYPE_VERSION_LOCKED_r11 }, + { 2, 2, _GUARD_TYPE_VERSION_LOCKED_r22 }, + { 3, 3, _GUARD_TYPE_VERSION_LOCKED_r33 }, + }, + }, + [_GUARD_TYPE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TYPE_r01 }, + { 1, 1, _GUARD_TYPE_r11 }, + { 2, 2, _GUARD_TYPE_r22 }, + { 3, 3, _GUARD_TYPE_r33 }, + }, + }, + [_CHECK_MANAGED_OBJECT_HAS_VALUES] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _CHECK_MANAGED_OBJECT_HAS_VALUES_r01 }, + { 1, 1, _CHECK_MANAGED_OBJECT_HAS_VALUES_r11 }, + { 2, 2, _CHECK_MANAGED_OBJECT_HAS_VALUES_r22 }, + { 3, 3, _CHECK_MANAGED_OBJECT_HAS_VALUES_r33 }, + }, + }, + [_LOAD_ATTR_INSTANCE_VALUE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _LOAD_ATTR_INSTANCE_VALUE_r02 }, + { 2, 1, _LOAD_ATTR_INSTANCE_VALUE_r12 }, + { 3, 2, _LOAD_ATTR_INSTANCE_VALUE_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_ATTR_MODULE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _LOAD_ATTR_MODULE_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_ATTR_WITH_HINT] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _LOAD_ATTR_WITH_HINT_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_ATTR_SLOT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _LOAD_ATTR_SLOT_r02 }, + { 2, 1, _LOAD_ATTR_SLOT_r12 }, + { 3, 2, _LOAD_ATTR_SLOT_r23 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_ATTR_CLASS] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _CHECK_ATTR_CLASS_r01 }, + { 1, 1, _CHECK_ATTR_CLASS_r11 }, + { 2, 2, _CHECK_ATTR_CLASS_r22 }, + { 3, 3, _CHECK_ATTR_CLASS_r33 }, + }, + }, + [_LOAD_ATTR_CLASS] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _LOAD_ATTR_CLASS_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_ATTR_PROPERTY_FRAME] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _LOAD_ATTR_PROPERTY_FRAME_r01 }, + { 1, 1, _LOAD_ATTR_PROPERTY_FRAME_r11 }, + { 2, 2, _LOAD_ATTR_PROPERTY_FRAME_r22 }, + { 3, 3, _LOAD_ATTR_PROPERTY_FRAME_r33 }, + }, + }, + [_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_DORV_NO_DICT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_DORV_NO_DICT_r01 }, + { 1, 1, _GUARD_DORV_NO_DICT_r11 }, + { 2, 2, _GUARD_DORV_NO_DICT_r22 }, + { 3, 3, _GUARD_DORV_NO_DICT_r33 }, + }, + }, + [_STORE_ATTR_INSTANCE_VALUE] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _STORE_ATTR_INSTANCE_VALUE_r21 }, + { -1, -1, -1 }, + }, + }, + [_LOCK_OBJECT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _LOCK_OBJECT_r01 }, + { 1, 1, _LOCK_OBJECT_r11 }, + { 2, 2, _LOCK_OBJECT_r22 }, + { 3, 3, _LOCK_OBJECT_r33 }, + }, + }, + [_STORE_ATTR_WITH_HINT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _STORE_ATTR_WITH_HINT_r21 }, + { -1, -1, -1 }, + }, + }, + [_STORE_ATTR_SLOT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _STORE_ATTR_SLOT_r21 }, + { -1, -1, -1 }, + }, + }, + [_COMPARE_OP] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _COMPARE_OP_r21 }, + { -1, -1, -1 }, + }, + }, + [_COMPARE_OP_FLOAT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _COMPARE_OP_FLOAT_r03 }, + { 3, 1, _COMPARE_OP_FLOAT_r13 }, + { 3, 2, _COMPARE_OP_FLOAT_r23 }, + { -1, -1, -1 }, + }, + }, + [_COMPARE_OP_INT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _COMPARE_OP_INT_r23 }, + { -1, -1, -1 }, + }, + }, + [_COMPARE_OP_STR] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _COMPARE_OP_STR_r23 }, + { -1, -1, -1 }, + }, + }, + [_IS_OP] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _IS_OP_r03 }, + { 3, 1, _IS_OP_r13 }, + { 3, 2, _IS_OP_r23 }, + { -1, -1, -1 }, + }, + }, + [_CONTAINS_OP] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _CONTAINS_OP_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_TOS_ANY_SET] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_ANY_SET_r01 }, + { 1, 1, _GUARD_TOS_ANY_SET_r11 }, + { 2, 2, _GUARD_TOS_ANY_SET_r22 }, + { 3, 3, _GUARD_TOS_ANY_SET_r33 }, + }, + }, + [_GUARD_TOS_SET] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_SET_r01 }, + { 1, 1, _GUARD_TOS_SET_r11 }, + { 2, 2, _GUARD_TOS_SET_r22 }, + { 3, 3, _GUARD_TOS_SET_r33 }, + }, + }, + [_GUARD_TOS_FROZENSET] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_FROZENSET_r01 }, + { 1, 1, _GUARD_TOS_FROZENSET_r11 }, + { 2, 2, _GUARD_TOS_FROZENSET_r22 }, + { 3, 3, _GUARD_TOS_FROZENSET_r33 }, + }, + }, + [_CONTAINS_OP_SET] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _CONTAINS_OP_SET_r23 }, + { -1, -1, -1 }, + }, + }, + [_CONTAINS_OP_DICT] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _CONTAINS_OP_DICT_r23 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_EG_MATCH] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 2, 2, _CHECK_EG_MATCH_r22 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_EXC_MATCH] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 2, 2, _CHECK_EXC_MATCH_r22 }, + { -1, -1, -1 }, + }, + }, + [_IMPORT_NAME] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _IMPORT_NAME_r21 }, + { -1, -1, -1 }, + }, + }, + [_IMPORT_FROM] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _IMPORT_FROM_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_IS_NONE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _IS_NONE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GET_LEN] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _GET_LEN_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_MATCH_CLASS] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 3, _MATCH_CLASS_r33 }, + }, + }, + [_MATCH_MAPPING] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _MATCH_MAPPING_r02 }, + { 2, 1, _MATCH_MAPPING_r12 }, + { 3, 2, _MATCH_MAPPING_r23 }, + { -1, -1, -1 }, + }, + }, + [_MATCH_SEQUENCE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _MATCH_SEQUENCE_r02 }, + { 2, 1, _MATCH_SEQUENCE_r12 }, + { 3, 2, _MATCH_SEQUENCE_r23 }, + { -1, -1, -1 }, + }, + }, + [_MATCH_KEYS] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _MATCH_KEYS_r23 }, + { -1, -1, -1 }, + }, + }, + [_GET_ITER] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _GET_ITER_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_ITERATOR] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_ITERATOR_r01 }, + { 1, 1, _GUARD_ITERATOR_r11 }, + { 2, 2, _GUARD_ITERATOR_r22 }, + { 3, 3, _GUARD_ITERATOR_r33 }, + }, + }, + [_GUARD_ITER_VIRTUAL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_ITER_VIRTUAL_r01 }, + { 1, 1, _GUARD_ITER_VIRTUAL_r11 }, + { 2, 2, _GUARD_ITER_VIRTUAL_r22 }, + { 3, 3, _GUARD_ITER_VIRTUAL_r33 }, + }, + }, + [_PUSH_TAGGED_ZERO] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _PUSH_TAGGED_ZERO_r01 }, + { 2, 1, _PUSH_TAGGED_ZERO_r12 }, + { 3, 2, _PUSH_TAGGED_ZERO_r23 }, + { -1, -1, -1 }, + }, + }, + [_GET_ITER_TRAD] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _GET_ITER_TRAD_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_FOR_ITER_TIER_TWO] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _FOR_ITER_TIER_TWO_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_TYPE_ITER] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_TYPE_ITER_r02 }, + { 2, 1, _GUARD_TYPE_ITER_r12 }, + { 2, 2, _GUARD_TYPE_ITER_r22 }, + { 3, 3, _GUARD_TYPE_ITER_r33 }, + }, + }, + [_ITER_NEXT_INLINE] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _ITER_NEXT_INLINE_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_NOS_ITER_VIRTUAL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_ITER_VIRTUAL_r02 }, + { 2, 1, _GUARD_NOS_ITER_VIRTUAL_r12 }, + { 2, 2, _GUARD_NOS_ITER_VIRTUAL_r22 }, + { 3, 3, _GUARD_NOS_ITER_VIRTUAL_r33 }, + }, + }, + [_GUARD_TOS_NOT_NULL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_TOS_NOT_NULL_r01 }, + { 1, 1, _GUARD_TOS_NOT_NULL_r11 }, + { 2, 2, _GUARD_TOS_NOT_NULL_r22 }, + { 3, 3, _GUARD_TOS_NOT_NULL_r33 }, + }, + }, + [_FOR_ITER_VIRTUAL_TIER_TWO] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _FOR_ITER_VIRTUAL_TIER_TWO_r23 }, + { -1, -1, -1 }, + }, + }, + [_ITER_CHECK_LIST] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _ITER_CHECK_LIST_r02 }, + { 2, 1, _ITER_CHECK_LIST_r12 }, + { 2, 2, _ITER_CHECK_LIST_r22 }, + { 3, 3, _ITER_CHECK_LIST_r33 }, + }, + }, + [_GUARD_NOT_EXHAUSTED_LIST] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOT_EXHAUSTED_LIST_r02 }, + { 2, 1, _GUARD_NOT_EXHAUSTED_LIST_r12 }, + { 2, 2, _GUARD_NOT_EXHAUSTED_LIST_r22 }, + { 3, 3, _GUARD_NOT_EXHAUSTED_LIST_r33 }, + }, + }, + [_ITER_NEXT_LIST_TIER_TWO] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _ITER_NEXT_LIST_TIER_TWO_r23 }, + { -1, -1, -1 }, + }, + }, + [_ITER_CHECK_TUPLE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _ITER_CHECK_TUPLE_r02 }, + { 2, 1, _ITER_CHECK_TUPLE_r12 }, + { 2, 2, _ITER_CHECK_TUPLE_r22 }, + { 3, 3, _ITER_CHECK_TUPLE_r33 }, + }, + }, + [_GUARD_NOT_EXHAUSTED_TUPLE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOT_EXHAUSTED_TUPLE_r02 }, + { 2, 1, _GUARD_NOT_EXHAUSTED_TUPLE_r12 }, + { 2, 2, _GUARD_NOT_EXHAUSTED_TUPLE_r22 }, + { 3, 3, _GUARD_NOT_EXHAUSTED_TUPLE_r33 }, + }, + }, + [_ITER_NEXT_TUPLE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _ITER_NEXT_TUPLE_r03 }, + { 3, 1, _ITER_NEXT_TUPLE_r13 }, + { 3, 2, _ITER_NEXT_TUPLE_r23 }, + { -1, -1, -1 }, + }, + }, + [_ITER_CHECK_RANGE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _ITER_CHECK_RANGE_r02 }, + { 2, 1, _ITER_CHECK_RANGE_r12 }, + { 2, 2, _ITER_CHECK_RANGE_r22 }, + { 3, 3, _ITER_CHECK_RANGE_r33 }, + }, + }, + [_GUARD_NOT_EXHAUSTED_RANGE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOT_EXHAUSTED_RANGE_r02 }, + { 2, 1, _GUARD_NOT_EXHAUSTED_RANGE_r12 }, + { 2, 2, _GUARD_NOT_EXHAUSTED_RANGE_r22 }, + { 3, 3, _GUARD_NOT_EXHAUSTED_RANGE_r33 }, + }, + }, + [_ITER_NEXT_RANGE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _ITER_NEXT_RANGE_r03 }, + { 3, 1, _ITER_NEXT_RANGE_r13 }, + { 3, 2, _ITER_NEXT_RANGE_r23 }, + { -1, -1, -1 }, + }, + }, + [_FOR_ITER_GEN_FRAME] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _FOR_ITER_GEN_FRAME_r03 }, + { 3, 1, _FOR_ITER_GEN_FRAME_r13 }, + { 3, 2, _FOR_ITER_GEN_FRAME_r23 }, + { -1, -1, -1 }, + }, + }, + [_INSERT_NULL] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _INSERT_NULL_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_SPECIAL] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _LOAD_SPECIAL_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_WITH_EXCEPT_START] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 3, _WITH_EXCEPT_START_r33 }, + }, + }, + [_PUSH_EXC_INFO] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _PUSH_EXC_INFO_r02 }, + { 2, 1, _PUSH_EXC_INFO_r12 }, + { 3, 2, _PUSH_EXC_INFO_r23 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r01 }, + { 1, 1, _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r11 }, + { 2, 2, _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r22 }, + { 3, 3, _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r33 }, + }, + }, + [_GUARD_KEYS_VERSION] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _GUARD_KEYS_VERSION_r01 }, + { 1, 1, _GUARD_KEYS_VERSION_r11 }, + { 2, 2, _GUARD_KEYS_VERSION_r22 }, + { 3, 3, _GUARD_KEYS_VERSION_r33 }, + }, + }, + [_LOAD_ATTR_METHOD_WITH_VALUES] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _LOAD_ATTR_METHOD_WITH_VALUES_r02 }, + { 2, 1, _LOAD_ATTR_METHOD_WITH_VALUES_r12 }, + { 3, 2, _LOAD_ATTR_METHOD_WITH_VALUES_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_ATTR_METHOD_NO_DICT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _LOAD_ATTR_METHOD_NO_DICT_r02 }, + { 2, 1, _LOAD_ATTR_METHOD_NO_DICT_r12 }, + { 3, 2, _LOAD_ATTR_METHOD_NO_DICT_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _LOAD_ATTR_NONDESCRIPTOR_NO_DICT_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_ATTR_METHOD_LAZY_DICT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _CHECK_ATTR_METHOD_LAZY_DICT_r01 }, + { 1, 1, _CHECK_ATTR_METHOD_LAZY_DICT_r11 }, + { 2, 2, _CHECK_ATTR_METHOD_LAZY_DICT_r22 }, + { 3, 3, _CHECK_ATTR_METHOD_LAZY_DICT_r33 }, + }, + }, + [_LOAD_ATTR_METHOD_LAZY_DICT] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _LOAD_ATTR_METHOD_LAZY_DICT_r02 }, + { 2, 1, _LOAD_ATTR_METHOD_LAZY_DICT_r12 }, + { 3, 2, _LOAD_ATTR_METHOD_LAZY_DICT_r23 }, + { -1, -1, -1 }, + }, + }, + [_MAYBE_EXPAND_METHOD] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _MAYBE_EXPAND_METHOD_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_PY_FRAME_GENERAL] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 1, _PY_FRAME_GENERAL_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_FUNCTION_VERSION] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_FUNCTION_VERSION_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_FUNCTION_VERSION_INLINE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _CHECK_FUNCTION_VERSION_INLINE_r00 }, + { 1, 1, _CHECK_FUNCTION_VERSION_INLINE_r11 }, + { 2, 2, _CHECK_FUNCTION_VERSION_INLINE_r22 }, + { 3, 3, _CHECK_FUNCTION_VERSION_INLINE_r33 }, + }, + }, + [_CHECK_METHOD_VERSION] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_METHOD_VERSION_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_EXPAND_METHOD] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _EXPAND_METHOD_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_IS_NOT_PY_CALLABLE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_IS_NOT_PY_CALLABLE_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_NON_PY_GENERAL] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _CALL_NON_PY_GENERAL_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_CALL_BOUND_METHOD_EXACT_ARGS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_CALL_BOUND_METHOD_EXACT_ARGS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_INIT_CALL_BOUND_METHOD_EXACT_ARGS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _INIT_CALL_BOUND_METHOD_EXACT_ARGS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_PEP_523] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _CHECK_PEP_523_r00 }, + { 1, 1, _CHECK_PEP_523_r11 }, + { 2, 2, _CHECK_PEP_523_r22 }, + { 3, 3, _CHECK_PEP_523_r33 }, + }, + }, + [_CHECK_FUNCTION_EXACT_ARGS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_FUNCTION_EXACT_ARGS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_STACK_SPACE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_STACK_SPACE_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_RECURSION_REMAINING] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _CHECK_RECURSION_REMAINING_r00 }, + { 1, 1, _CHECK_RECURSION_REMAINING_r11 }, + { 2, 2, _CHECK_RECURSION_REMAINING_r22 }, + { 3, 3, _CHECK_RECURSION_REMAINING_r33 }, + }, + }, + [_INIT_CALL_PY_EXACT_ARGS_0] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _INIT_CALL_PY_EXACT_ARGS_0_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_INIT_CALL_PY_EXACT_ARGS_1] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _INIT_CALL_PY_EXACT_ARGS_1_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_INIT_CALL_PY_EXACT_ARGS_2] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _INIT_CALL_PY_EXACT_ARGS_2_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_INIT_CALL_PY_EXACT_ARGS_3] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _INIT_CALL_PY_EXACT_ARGS_3_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_INIT_CALL_PY_EXACT_ARGS_4] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _INIT_CALL_PY_EXACT_ARGS_4_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_INIT_CALL_PY_EXACT_ARGS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _INIT_CALL_PY_EXACT_ARGS_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_PUSH_FRAME] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 0, _PUSH_FRAME_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_NOS_NULL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _GUARD_NOS_NULL_r02 }, + { 2, 1, _GUARD_NOS_NULL_r12 }, + { 2, 2, _GUARD_NOS_NULL_r22 }, + { 3, 3, _GUARD_NOS_NULL_r33 }, + }, + }, + [_GUARD_THIRD_NULL] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_THIRD_NULL_r03 }, + { 3, 1, _GUARD_THIRD_NULL_r13 }, + { 3, 2, _GUARD_THIRD_NULL_r23 }, + { 3, 3, _GUARD_THIRD_NULL_r33 }, + }, + }, + [_GUARD_CALLABLE_TYPE_1] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_CALLABLE_TYPE_1_r03 }, + { 3, 1, _GUARD_CALLABLE_TYPE_1_r13 }, + { 3, 2, _GUARD_CALLABLE_TYPE_1_r23 }, + { 3, 3, _GUARD_CALLABLE_TYPE_1_r33 }, + }, + }, + [_CALL_TYPE_1] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _CALL_TYPE_1_r02 }, + { 2, 1, _CALL_TYPE_1_r12 }, + { 2, 2, _CALL_TYPE_1_r22 }, + { 2, 3, _CALL_TYPE_1_r32 }, + }, + }, + [_GUARD_CALLABLE_STR_1] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_CALLABLE_STR_1_r03 }, + { 3, 1, _GUARD_CALLABLE_STR_1_r13 }, + { 3, 2, _GUARD_CALLABLE_STR_1_r23 }, + { 3, 3, _GUARD_CALLABLE_STR_1_r33 }, + }, + }, + [_CALL_STR_1] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 2, 3, _CALL_STR_1_r32 }, + }, + }, + [_GUARD_CALLABLE_TUPLE_1] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_CALLABLE_TUPLE_1_r03 }, + { 3, 1, _GUARD_CALLABLE_TUPLE_1_r13 }, + { 3, 2, _GUARD_CALLABLE_TUPLE_1_r23 }, + { 3, 3, _GUARD_CALLABLE_TUPLE_1_r33 }, + }, + }, + [_CALL_TUPLE_1] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 2, 3, _CALL_TUPLE_1_r32 }, + }, + }, + [_CHECK_OBJECT] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CHECK_OBJECT_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_ALLOCATE_OBJECT] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _ALLOCATE_OBJECT_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CREATE_INIT_FRAME] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 1, _CREATE_INIT_FRAME_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_EXIT_INIT_CHECK] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 1, _EXIT_INIT_CHECK_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_BUILTIN_CLASS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_BUILTIN_CLASS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_BUILTIN_CLASS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CALL_BUILTIN_CLASS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_BUILTIN_O] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_BUILTIN_O_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_BUILTIN_O] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 3, 0, _CALL_BUILTIN_O_r03 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_BUILTIN_FAST] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_BUILTIN_FAST_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_BUILTIN_FAST] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CALL_BUILTIN_FAST_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_BUILTIN_FAST_WITH_KEYWORDS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CALL_BUILTIN_FAST_WITH_KEYWORDS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_LEN] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_CALLABLE_LEN_r03 }, + { 3, 1, _GUARD_CALLABLE_LEN_r13 }, + { 3, 2, _GUARD_CALLABLE_LEN_r23 }, + { 3, 3, _GUARD_CALLABLE_LEN_r33 }, + }, + }, + [_CALL_LEN] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 3, _CALL_LEN_r33 }, + }, + }, + [_GUARD_CALLABLE_ISINSTANCE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_CALLABLE_ISINSTANCE_r03 }, + { 3, 1, _GUARD_CALLABLE_ISINSTANCE_r13 }, + { 3, 2, _GUARD_CALLABLE_ISINSTANCE_r23 }, + { 3, 3, _GUARD_CALLABLE_ISINSTANCE_r33 }, + }, + }, + [_CALL_ISINSTANCE] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 3, _CALL_ISINSTANCE_r31 }, + }, + }, + [_GUARD_CALLABLE_LIST_APPEND] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _GUARD_CALLABLE_LIST_APPEND_r03 }, + { 3, 1, _GUARD_CALLABLE_LIST_APPEND_r13 }, + { 3, 2, _GUARD_CALLABLE_LIST_APPEND_r23 }, + { 3, 3, _GUARD_CALLABLE_LIST_APPEND_r33 }, + }, + }, + [_CALL_LIST_APPEND] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _CALL_LIST_APPEND_r03 }, + { 3, 1, _CALL_LIST_APPEND_r13 }, + { 3, 2, _CALL_LIST_APPEND_r23 }, + { 3, 3, _CALL_LIST_APPEND_r33 }, + }, + }, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_O] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_METHOD_DESCRIPTOR_O_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_O] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 3, 0, _CALL_METHOD_DESCRIPTOR_O_r03 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_RECURSION_LIMIT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _CHECK_RECURSION_LIMIT_r00 }, + { 1, 1, _CHECK_RECURSION_LIMIT_r11 }, + { 2, 2, _CHECK_RECURSION_LIMIT_r22 }, + { 3, 3, _CHECK_RECURSION_LIMIT_r33 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_O_INLINE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 3, 0, _CALL_METHOD_DESCRIPTOR_O_INLINE_r03 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_NOARGS] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 3, 0, _CALL_METHOD_DESCRIPTOR_NOARGS_r03 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 3, 0, _CALL_METHOD_DESCRIPTOR_NOARGS_INLINE_r03 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_FAST] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CALL_METHOD_DESCRIPTOR_FAST_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_METHOD_DESCRIPTOR_FAST_INLINE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _CALL_METHOD_DESCRIPTOR_FAST_INLINE_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_MAYBE_EXPAND_METHOD_KW] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _MAYBE_EXPAND_METHOD_KW_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_PY_FRAME_KW] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _PY_FRAME_KW_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_FUNCTION_VERSION_KW] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _CHECK_FUNCTION_VERSION_KW_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_METHOD_VERSION_KW] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _CHECK_METHOD_VERSION_KW_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_EXPAND_METHOD_KW] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _EXPAND_METHOD_KW_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CHECK_IS_NOT_PY_CALLABLE_KW] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _CHECK_IS_NOT_PY_CALLABLE_KW_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CALL_KW_NON_PY] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _CALL_KW_NON_PY_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_MAKE_CALLARGS_A_TUPLE] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 3, _MAKE_CALLARGS_A_TUPLE_r33 }, + }, + }, + [_CHECK_IS_PY_CALLABLE_EX] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _CHECK_IS_PY_CALLABLE_EX_r03 }, + { 3, 1, _CHECK_IS_PY_CALLABLE_EX_r13 }, + { 3, 2, _CHECK_IS_PY_CALLABLE_EX_r23 }, + { 3, 3, _CHECK_IS_PY_CALLABLE_EX_r33 }, + }, + }, + [_PY_FRAME_EX] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 1, _PY_FRAME_EX_r31 }, + }, + }, + [_CHECK_IS_NOT_PY_CALLABLE_EX] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _CHECK_IS_NOT_PY_CALLABLE_EX_r03 }, + { 3, 1, _CHECK_IS_NOT_PY_CALLABLE_EX_r13 }, + { 3, 2, _CHECK_IS_NOT_PY_CALLABLE_EX_r23 }, + { 3, 3, _CHECK_IS_NOT_PY_CALLABLE_EX_r33 }, + }, + }, + [_CALL_FUNCTION_EX_NON_PY_GENERAL] = { + .best = { 3, 3, 3, 3 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 3, _CALL_FUNCTION_EX_NON_PY_GENERAL_r31 }, + }, + }, + [_MAKE_FUNCTION] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 2, 1, _MAKE_FUNCTION_r12 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_SET_FUNCTION_ATTRIBUTE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 1, 0, _SET_FUNCTION_ATTRIBUTE_r01 }, + { 1, 1, _SET_FUNCTION_ATTRIBUTE_r11 }, + { 1, 2, _SET_FUNCTION_ATTRIBUTE_r21 }, + { 2, 3, _SET_FUNCTION_ATTRIBUTE_r32 }, + }, + }, + [_RETURN_GENERATOR] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _RETURN_GENERATOR_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_BUILD_SLICE] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _BUILD_SLICE_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_CONVERT_VALUE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _CONVERT_VALUE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_FORMAT_SIMPLE] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _FORMAT_SIMPLE_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_FORMAT_WITH_SPEC] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 1, 2, _FORMAT_WITH_SPEC_r21 }, + { -1, -1, -1 }, + }, + }, + [_COPY_1] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 2, 0, _COPY_1_r02 }, + { 2, 1, _COPY_1_r12 }, + { 3, 2, _COPY_1_r23 }, + { -1, -1, -1 }, + }, + }, + [_COPY_2] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 3, 0, _COPY_2_r03 }, + { 3, 1, _COPY_2_r13 }, + { 3, 2, _COPY_2_r23 }, + { -1, -1, -1 }, + }, + }, + [_COPY_3] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _COPY_3_r03 }, + { 3, 1, _COPY_3_r13 }, + { 3, 2, _COPY_3_r23 }, + { 3, 3, _COPY_3_r33 }, + }, + }, + [_COPY] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 1, 0, _COPY_r01 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_BINARY_OP] = { + .best = { 2, 2, 2, 2 }, + .entries = { + { -1, -1, -1 }, + { -1, -1, -1 }, + { 3, 2, _BINARY_OP_r23 }, + { -1, -1, -1 }, + }, + }, + [_SWAP_2] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 2, 0, _SWAP_2_r02 }, + { 2, 1, _SWAP_2_r12 }, + { 2, 2, _SWAP_2_r22 }, + { 3, 3, _SWAP_2_r33 }, + }, + }, + [_SWAP_3] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _SWAP_3_r03 }, + { 3, 1, _SWAP_3_r13 }, + { 3, 2, _SWAP_3_r23 }, + { 3, 3, _SWAP_3_r33 }, + }, + }, + [_SWAP] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 1, 1, _SWAP_r11 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_IS_TRUE_POP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_IS_TRUE_POP_r00 }, + { 0, 0, _GUARD_IS_TRUE_POP_r10 }, + { 1, 1, _GUARD_IS_TRUE_POP_r21 }, + { 2, 2, _GUARD_IS_TRUE_POP_r32 }, + }, + }, + [_GUARD_IS_FALSE_POP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_IS_FALSE_POP_r00 }, + { 0, 0, _GUARD_IS_FALSE_POP_r10 }, + { 1, 1, _GUARD_IS_FALSE_POP_r21 }, + { 2, 2, _GUARD_IS_FALSE_POP_r32 }, + }, + }, + [_GUARD_BIT_IS_SET_POP_4] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_SET_POP_4_r00 }, + { 0, 0, _GUARD_BIT_IS_SET_POP_4_r10 }, + { 1, 1, _GUARD_BIT_IS_SET_POP_4_r21 }, + { 2, 2, _GUARD_BIT_IS_SET_POP_4_r32 }, + }, + }, + [_GUARD_BIT_IS_SET_POP_5] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_SET_POP_5_r00 }, + { 0, 0, _GUARD_BIT_IS_SET_POP_5_r10 }, + { 1, 1, _GUARD_BIT_IS_SET_POP_5_r21 }, + { 2, 2, _GUARD_BIT_IS_SET_POP_5_r32 }, + }, + }, + [_GUARD_BIT_IS_SET_POP_6] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_SET_POP_6_r00 }, + { 0, 0, _GUARD_BIT_IS_SET_POP_6_r10 }, + { 1, 1, _GUARD_BIT_IS_SET_POP_6_r21 }, + { 2, 2, _GUARD_BIT_IS_SET_POP_6_r32 }, + }, + }, + [_GUARD_BIT_IS_SET_POP_7] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_SET_POP_7_r00 }, + { 0, 0, _GUARD_BIT_IS_SET_POP_7_r10 }, + { 1, 1, _GUARD_BIT_IS_SET_POP_7_r21 }, + { 2, 2, _GUARD_BIT_IS_SET_POP_7_r32 }, + }, + }, + [_GUARD_BIT_IS_SET_POP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_SET_POP_r00 }, + { 0, 0, _GUARD_BIT_IS_SET_POP_r10 }, + { 1, 1, _GUARD_BIT_IS_SET_POP_r21 }, + { 2, 2, _GUARD_BIT_IS_SET_POP_r32 }, + }, + }, + [_GUARD_BIT_IS_UNSET_POP_4] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_UNSET_POP_4_r00 }, + { 0, 0, _GUARD_BIT_IS_UNSET_POP_4_r10 }, + { 1, 1, _GUARD_BIT_IS_UNSET_POP_4_r21 }, + { 2, 2, _GUARD_BIT_IS_UNSET_POP_4_r32 }, + }, + }, + [_GUARD_BIT_IS_UNSET_POP_5] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_UNSET_POP_5_r00 }, + { 0, 0, _GUARD_BIT_IS_UNSET_POP_5_r10 }, + { 1, 1, _GUARD_BIT_IS_UNSET_POP_5_r21 }, + { 2, 2, _GUARD_BIT_IS_UNSET_POP_5_r32 }, + }, + }, + [_GUARD_BIT_IS_UNSET_POP_6] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_UNSET_POP_6_r00 }, + { 0, 0, _GUARD_BIT_IS_UNSET_POP_6_r10 }, + { 1, 1, _GUARD_BIT_IS_UNSET_POP_6_r21 }, + { 2, 2, _GUARD_BIT_IS_UNSET_POP_6_r32 }, + }, + }, + [_GUARD_BIT_IS_UNSET_POP_7] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_UNSET_POP_7_r00 }, + { 0, 0, _GUARD_BIT_IS_UNSET_POP_7_r10 }, + { 1, 1, _GUARD_BIT_IS_UNSET_POP_7_r21 }, + { 2, 2, _GUARD_BIT_IS_UNSET_POP_7_r32 }, + }, + }, + [_GUARD_BIT_IS_UNSET_POP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_BIT_IS_UNSET_POP_r00 }, + { 0, 0, _GUARD_BIT_IS_UNSET_POP_r10 }, + { 1, 1, _GUARD_BIT_IS_UNSET_POP_r21 }, + { 2, 2, _GUARD_BIT_IS_UNSET_POP_r32 }, + }, + }, + [_GUARD_IS_NONE_POP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_IS_NONE_POP_r00 }, + { 0, 0, _GUARD_IS_NONE_POP_r10 }, + { 1, 1, _GUARD_IS_NONE_POP_r21 }, + { 2, 2, _GUARD_IS_NONE_POP_r32 }, + }, + }, + [_GUARD_IS_NOT_NONE_POP] = { + .best = { 1, 1, 1, 1 }, + .entries = { + { -1, -1, -1 }, + { 0, 0, _GUARD_IS_NOT_NONE_POP_r10 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_JUMP_TO_TOP] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _JUMP_TO_TOP_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_SET_IP] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _SET_IP_r00 }, + { 1, 1, _SET_IP_r11 }, + { 2, 2, _SET_IP_r22 }, + { 3, 3, _SET_IP_r33 }, + }, + }, + [_CHECK_STACK_SPACE_OPERAND] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _CHECK_STACK_SPACE_OPERAND_r00 }, + { 1, 1, _CHECK_STACK_SPACE_OPERAND_r11 }, + { 2, 2, _CHECK_STACK_SPACE_OPERAND_r22 }, + { 3, 3, _CHECK_STACK_SPACE_OPERAND_r33 }, + }, + }, + [_SAVE_RETURN_OFFSET] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _SAVE_RETURN_OFFSET_r00 }, + { 1, 1, _SAVE_RETURN_OFFSET_r11 }, + { 2, 2, _SAVE_RETURN_OFFSET_r22 }, + { 3, 3, _SAVE_RETURN_OFFSET_r33 }, + }, + }, + [_EXIT_TRACE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _EXIT_TRACE_r00 }, + { 0, 0, _EXIT_TRACE_r10 }, + { 0, 0, _EXIT_TRACE_r20 }, + { 0, 0, _EXIT_TRACE_r30 }, + }, + }, + [_DYNAMIC_EXIT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _DYNAMIC_EXIT_r00 }, + { 0, 0, _DYNAMIC_EXIT_r10 }, + { 0, 0, _DYNAMIC_EXIT_r20 }, + { 0, 0, _DYNAMIC_EXIT_r30 }, + }, + }, + [_CHECK_VALIDITY] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _CHECK_VALIDITY_r00 }, + { 1, 1, _CHECK_VALIDITY_r11 }, + { 2, 2, _CHECK_VALIDITY_r22 }, + { 3, 3, _CHECK_VALIDITY_r33 }, + }, + }, + [_LOAD_CONST_INLINE] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_CONST_INLINE_r01 }, + { 2, 1, _LOAD_CONST_INLINE_r12 }, + { 3, 2, _LOAD_CONST_INLINE_r23 }, + { -1, -1, -1 }, + }, + }, + [_LOAD_CONST_INLINE_BORROW] = { + .best = { 0, 1, 2, 2 }, + .entries = { + { 1, 0, _LOAD_CONST_INLINE_BORROW_r01 }, + { 2, 1, _LOAD_CONST_INLINE_BORROW_r12 }, + { 3, 2, _LOAD_CONST_INLINE_BORROW_r23 }, + { -1, -1, -1 }, + }, + }, + [_RROT_3] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 3, 0, _RROT_3_r03 }, + { 3, 1, _RROT_3_r13 }, + { 3, 2, _RROT_3_r23 }, + { 3, 3, _RROT_3_r33 }, + }, + }, + [_START_EXECUTOR] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _START_EXECUTOR_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_MAKE_WARM] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _MAKE_WARM_r00 }, + { 1, 1, _MAKE_WARM_r11 }, + { 2, 2, _MAKE_WARM_r22 }, + { 3, 3, _MAKE_WARM_r33 }, + }, + }, + [_FATAL_ERROR] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _FATAL_ERROR_r00 }, + { 1, 1, _FATAL_ERROR_r11 }, + { 2, 2, _FATAL_ERROR_r22 }, + { 3, 3, _FATAL_ERROR_r33 }, + }, + }, + [_DEOPT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _DEOPT_r00 }, + { 0, 0, _DEOPT_r10 }, + { 0, 0, _DEOPT_r20 }, + { 0, 0, _DEOPT_r30 }, + }, + }, + [_HANDLE_PENDING_AND_DEOPT] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _HANDLE_PENDING_AND_DEOPT_r00 }, + { 0, 0, _HANDLE_PENDING_AND_DEOPT_r10 }, + { 0, 0, _HANDLE_PENDING_AND_DEOPT_r20 }, + { 0, 0, _HANDLE_PENDING_AND_DEOPT_r30 }, + }, + }, + [_ERROR_POP_N] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _ERROR_POP_N_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_TIER2_RESUME_CHECK] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _TIER2_RESUME_CHECK_r00 }, + { 1, 1, _TIER2_RESUME_CHECK_r11 }, + { 2, 2, _TIER2_RESUME_CHECK_r22 }, + { 3, 3, _TIER2_RESUME_CHECK_r33 }, + }, + }, + [_COLD_EXIT] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _COLD_EXIT_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_COLD_DYNAMIC_EXIT] = { + .best = { 0, 0, 0, 0 }, + .entries = { + { 0, 0, _COLD_DYNAMIC_EXIT_r00 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + { -1, -1, -1 }, + }, + }, + [_GUARD_CODE_VERSION__PUSH_FRAME] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_CODE_VERSION__PUSH_FRAME_r00 }, + { 1, 1, _GUARD_CODE_VERSION__PUSH_FRAME_r11 }, + { 2, 2, _GUARD_CODE_VERSION__PUSH_FRAME_r22 }, + { 3, 3, _GUARD_CODE_VERSION__PUSH_FRAME_r33 }, + }, + }, + [_GUARD_CODE_VERSION_YIELD_VALUE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_CODE_VERSION_YIELD_VALUE_r00 }, + { 1, 1, _GUARD_CODE_VERSION_YIELD_VALUE_r11 }, + { 2, 2, _GUARD_CODE_VERSION_YIELD_VALUE_r22 }, + { 3, 3, _GUARD_CODE_VERSION_YIELD_VALUE_r33 }, + }, + }, + [_GUARD_CODE_VERSION_RETURN_VALUE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_CODE_VERSION_RETURN_VALUE_r00 }, + { 1, 1, _GUARD_CODE_VERSION_RETURN_VALUE_r11 }, + { 2, 2, _GUARD_CODE_VERSION_RETURN_VALUE_r22 }, + { 3, 3, _GUARD_CODE_VERSION_RETURN_VALUE_r33 }, + }, + }, + [_GUARD_CODE_VERSION_RETURN_GENERATOR] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_CODE_VERSION_RETURN_GENERATOR_r00 }, + { 1, 1, _GUARD_CODE_VERSION_RETURN_GENERATOR_r11 }, + { 2, 2, _GUARD_CODE_VERSION_RETURN_GENERATOR_r22 }, + { 3, 3, _GUARD_CODE_VERSION_RETURN_GENERATOR_r33 }, + }, + }, + [_GUARD_IP__PUSH_FRAME] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_IP__PUSH_FRAME_r00 }, + { 1, 1, _GUARD_IP__PUSH_FRAME_r11 }, + { 2, 2, _GUARD_IP__PUSH_FRAME_r22 }, + { 3, 3, _GUARD_IP__PUSH_FRAME_r33 }, + }, + }, + [_GUARD_IP_YIELD_VALUE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_IP_YIELD_VALUE_r00 }, + { 1, 1, _GUARD_IP_YIELD_VALUE_r11 }, + { 2, 2, _GUARD_IP_YIELD_VALUE_r22 }, + { 3, 3, _GUARD_IP_YIELD_VALUE_r33 }, + }, + }, + [_GUARD_IP_RETURN_VALUE] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_IP_RETURN_VALUE_r00 }, + { 1, 1, _GUARD_IP_RETURN_VALUE_r11 }, + { 2, 2, _GUARD_IP_RETURN_VALUE_r22 }, + { 3, 3, _GUARD_IP_RETURN_VALUE_r33 }, + }, + }, + [_GUARD_IP_RETURN_GENERATOR] = { + .best = { 0, 1, 2, 3 }, + .entries = { + { 0, 0, _GUARD_IP_RETURN_GENERATOR_r00 }, + { 1, 1, _GUARD_IP_RETURN_GENERATOR_r11 }, + { 2, 2, _GUARD_IP_RETURN_GENERATOR_r22 }, + { 3, 3, _GUARD_IP_RETURN_GENERATOR_r33 }, + }, + }, }; -const uint8_t _PyUop_Replication[MAX_UOP_ID+1] = { - [_LOAD_FAST] = 8, - [_LOAD_FAST_BORROW] = 8, - [_LOAD_SMALL_INT] = 4, - [_STORE_FAST] = 8, - [_INIT_CALL_PY_EXACT_ARGS] = 5, +const uint16_t _PyUop_Uncached[MAX_UOP_REGS_ID+1] = { + [_NOP_r00] = _NOP, + [_NOP_r11] = _NOP, + [_NOP_r22] = _NOP, + [_NOP_r33] = _NOP, + [_CHECK_PERIODIC_r00] = _CHECK_PERIODIC, + [_CHECK_PERIODIC_IF_NOT_YIELD_FROM_r00] = _CHECK_PERIODIC_IF_NOT_YIELD_FROM, + [_RESUME_CHECK_r00] = _RESUME_CHECK, + [_RESUME_CHECK_r11] = _RESUME_CHECK, + [_RESUME_CHECK_r22] = _RESUME_CHECK, + [_RESUME_CHECK_r33] = _RESUME_CHECK, + [_LOAD_FAST_CHECK_r01] = _LOAD_FAST_CHECK, + [_LOAD_FAST_CHECK_r12] = _LOAD_FAST_CHECK, + [_LOAD_FAST_CHECK_r23] = _LOAD_FAST_CHECK, + [_LOAD_FAST_0_r01] = _LOAD_FAST_0, + [_LOAD_FAST_0_r12] = _LOAD_FAST_0, + [_LOAD_FAST_0_r23] = _LOAD_FAST_0, + [_LOAD_FAST_1_r01] = _LOAD_FAST_1, + [_LOAD_FAST_1_r12] = _LOAD_FAST_1, + [_LOAD_FAST_1_r23] = _LOAD_FAST_1, + [_LOAD_FAST_2_r01] = _LOAD_FAST_2, + [_LOAD_FAST_2_r12] = _LOAD_FAST_2, + [_LOAD_FAST_2_r23] = _LOAD_FAST_2, + [_LOAD_FAST_3_r01] = _LOAD_FAST_3, + [_LOAD_FAST_3_r12] = _LOAD_FAST_3, + [_LOAD_FAST_3_r23] = _LOAD_FAST_3, + [_LOAD_FAST_4_r01] = _LOAD_FAST_4, + [_LOAD_FAST_4_r12] = _LOAD_FAST_4, + [_LOAD_FAST_4_r23] = _LOAD_FAST_4, + [_LOAD_FAST_5_r01] = _LOAD_FAST_5, + [_LOAD_FAST_5_r12] = _LOAD_FAST_5, + [_LOAD_FAST_5_r23] = _LOAD_FAST_5, + [_LOAD_FAST_6_r01] = _LOAD_FAST_6, + [_LOAD_FAST_6_r12] = _LOAD_FAST_6, + [_LOAD_FAST_6_r23] = _LOAD_FAST_6, + [_LOAD_FAST_7_r01] = _LOAD_FAST_7, + [_LOAD_FAST_7_r12] = _LOAD_FAST_7, + [_LOAD_FAST_7_r23] = _LOAD_FAST_7, + [_LOAD_FAST_r01] = _LOAD_FAST, + [_LOAD_FAST_r12] = _LOAD_FAST, + [_LOAD_FAST_r23] = _LOAD_FAST, + [_LOAD_FAST_BORROW_0_r01] = _LOAD_FAST_BORROW_0, + [_LOAD_FAST_BORROW_0_r12] = _LOAD_FAST_BORROW_0, + [_LOAD_FAST_BORROW_0_r23] = _LOAD_FAST_BORROW_0, + [_LOAD_FAST_BORROW_1_r01] = _LOAD_FAST_BORROW_1, + [_LOAD_FAST_BORROW_1_r12] = _LOAD_FAST_BORROW_1, + [_LOAD_FAST_BORROW_1_r23] = _LOAD_FAST_BORROW_1, + [_LOAD_FAST_BORROW_2_r01] = _LOAD_FAST_BORROW_2, + [_LOAD_FAST_BORROW_2_r12] = _LOAD_FAST_BORROW_2, + [_LOAD_FAST_BORROW_2_r23] = _LOAD_FAST_BORROW_2, + [_LOAD_FAST_BORROW_3_r01] = _LOAD_FAST_BORROW_3, + [_LOAD_FAST_BORROW_3_r12] = _LOAD_FAST_BORROW_3, + [_LOAD_FAST_BORROW_3_r23] = _LOAD_FAST_BORROW_3, + [_LOAD_FAST_BORROW_4_r01] = _LOAD_FAST_BORROW_4, + [_LOAD_FAST_BORROW_4_r12] = _LOAD_FAST_BORROW_4, + [_LOAD_FAST_BORROW_4_r23] = _LOAD_FAST_BORROW_4, + [_LOAD_FAST_BORROW_5_r01] = _LOAD_FAST_BORROW_5, + [_LOAD_FAST_BORROW_5_r12] = _LOAD_FAST_BORROW_5, + [_LOAD_FAST_BORROW_5_r23] = _LOAD_FAST_BORROW_5, + [_LOAD_FAST_BORROW_6_r01] = _LOAD_FAST_BORROW_6, + [_LOAD_FAST_BORROW_6_r12] = _LOAD_FAST_BORROW_6, + [_LOAD_FAST_BORROW_6_r23] = _LOAD_FAST_BORROW_6, + [_LOAD_FAST_BORROW_7_r01] = _LOAD_FAST_BORROW_7, + [_LOAD_FAST_BORROW_7_r12] = _LOAD_FAST_BORROW_7, + [_LOAD_FAST_BORROW_7_r23] = _LOAD_FAST_BORROW_7, + [_LOAD_FAST_BORROW_r01] = _LOAD_FAST_BORROW, + [_LOAD_FAST_BORROW_r12] = _LOAD_FAST_BORROW, + [_LOAD_FAST_BORROW_r23] = _LOAD_FAST_BORROW, + [_LOAD_FAST_AND_CLEAR_r01] = _LOAD_FAST_AND_CLEAR, + [_LOAD_FAST_AND_CLEAR_r12] = _LOAD_FAST_AND_CLEAR, + [_LOAD_FAST_AND_CLEAR_r23] = _LOAD_FAST_AND_CLEAR, + [_LOAD_CONST_r01] = _LOAD_CONST, + [_LOAD_CONST_r12] = _LOAD_CONST, + [_LOAD_CONST_r23] = _LOAD_CONST, + [_LOAD_SMALL_INT_0_r01] = _LOAD_SMALL_INT_0, + [_LOAD_SMALL_INT_0_r12] = _LOAD_SMALL_INT_0, + [_LOAD_SMALL_INT_0_r23] = _LOAD_SMALL_INT_0, + [_LOAD_SMALL_INT_1_r01] = _LOAD_SMALL_INT_1, + [_LOAD_SMALL_INT_1_r12] = _LOAD_SMALL_INT_1, + [_LOAD_SMALL_INT_1_r23] = _LOAD_SMALL_INT_1, + [_LOAD_SMALL_INT_2_r01] = _LOAD_SMALL_INT_2, + [_LOAD_SMALL_INT_2_r12] = _LOAD_SMALL_INT_2, + [_LOAD_SMALL_INT_2_r23] = _LOAD_SMALL_INT_2, + [_LOAD_SMALL_INT_3_r01] = _LOAD_SMALL_INT_3, + [_LOAD_SMALL_INT_3_r12] = _LOAD_SMALL_INT_3, + [_LOAD_SMALL_INT_3_r23] = _LOAD_SMALL_INT_3, + [_LOAD_SMALL_INT_r01] = _LOAD_SMALL_INT, + [_LOAD_SMALL_INT_r12] = _LOAD_SMALL_INT, + [_LOAD_SMALL_INT_r23] = _LOAD_SMALL_INT, + [_SWAP_FAST_0_r01] = _SWAP_FAST_0, + [_SWAP_FAST_0_r11] = _SWAP_FAST_0, + [_SWAP_FAST_0_r22] = _SWAP_FAST_0, + [_SWAP_FAST_0_r33] = _SWAP_FAST_0, + [_SWAP_FAST_1_r01] = _SWAP_FAST_1, + [_SWAP_FAST_1_r11] = _SWAP_FAST_1, + [_SWAP_FAST_1_r22] = _SWAP_FAST_1, + [_SWAP_FAST_1_r33] = _SWAP_FAST_1, + [_SWAP_FAST_2_r01] = _SWAP_FAST_2, + [_SWAP_FAST_2_r11] = _SWAP_FAST_2, + [_SWAP_FAST_2_r22] = _SWAP_FAST_2, + [_SWAP_FAST_2_r33] = _SWAP_FAST_2, + [_SWAP_FAST_3_r01] = _SWAP_FAST_3, + [_SWAP_FAST_3_r11] = _SWAP_FAST_3, + [_SWAP_FAST_3_r22] = _SWAP_FAST_3, + [_SWAP_FAST_3_r33] = _SWAP_FAST_3, + [_SWAP_FAST_4_r01] = _SWAP_FAST_4, + [_SWAP_FAST_4_r11] = _SWAP_FAST_4, + [_SWAP_FAST_4_r22] = _SWAP_FAST_4, + [_SWAP_FAST_4_r33] = _SWAP_FAST_4, + [_SWAP_FAST_5_r01] = _SWAP_FAST_5, + [_SWAP_FAST_5_r11] = _SWAP_FAST_5, + [_SWAP_FAST_5_r22] = _SWAP_FAST_5, + [_SWAP_FAST_5_r33] = _SWAP_FAST_5, + [_SWAP_FAST_6_r01] = _SWAP_FAST_6, + [_SWAP_FAST_6_r11] = _SWAP_FAST_6, + [_SWAP_FAST_6_r22] = _SWAP_FAST_6, + [_SWAP_FAST_6_r33] = _SWAP_FAST_6, + [_SWAP_FAST_7_r01] = _SWAP_FAST_7, + [_SWAP_FAST_7_r11] = _SWAP_FAST_7, + [_SWAP_FAST_7_r22] = _SWAP_FAST_7, + [_SWAP_FAST_7_r33] = _SWAP_FAST_7, + [_SWAP_FAST_r01] = _SWAP_FAST, + [_SWAP_FAST_r11] = _SWAP_FAST, + [_SWAP_FAST_r22] = _SWAP_FAST, + [_SWAP_FAST_r33] = _SWAP_FAST, + [_POP_TOP_r10] = _POP_TOP, + [_POP_TOP_NOP_r00] = _POP_TOP_NOP, + [_POP_TOP_NOP_r10] = _POP_TOP_NOP, + [_POP_TOP_NOP_r21] = _POP_TOP_NOP, + [_POP_TOP_NOP_r32] = _POP_TOP_NOP, + [_POP_TOP_INT_r00] = _POP_TOP_INT, + [_POP_TOP_INT_r10] = _POP_TOP_INT, + [_POP_TOP_INT_r21] = _POP_TOP_INT, + [_POP_TOP_INT_r32] = _POP_TOP_INT, + [_POP_TOP_FLOAT_r00] = _POP_TOP_FLOAT, + [_POP_TOP_FLOAT_r10] = _POP_TOP_FLOAT, + [_POP_TOP_FLOAT_r21] = _POP_TOP_FLOAT, + [_POP_TOP_FLOAT_r32] = _POP_TOP_FLOAT, + [_POP_TOP_UNICODE_r00] = _POP_TOP_UNICODE, + [_POP_TOP_UNICODE_r10] = _POP_TOP_UNICODE, + [_POP_TOP_UNICODE_r21] = _POP_TOP_UNICODE, + [_POP_TOP_UNICODE_r32] = _POP_TOP_UNICODE, + [_POP_TOP_OPARG_r00] = _POP_TOP_OPARG, + [_PUSH_NULL_r01] = _PUSH_NULL, + [_PUSH_NULL_r12] = _PUSH_NULL, + [_PUSH_NULL_r23] = _PUSH_NULL, + [_END_FOR_r10] = _END_FOR, + [_POP_ITER_r20] = _POP_ITER, + [_END_SEND_r31] = _END_SEND, + [_UNARY_NEGATIVE_r12] = _UNARY_NEGATIVE, + [_UNARY_NEGATIVE_FLOAT_INPLACE_r02] = _UNARY_NEGATIVE_FLOAT_INPLACE, + [_UNARY_NEGATIVE_FLOAT_INPLACE_r12] = _UNARY_NEGATIVE_FLOAT_INPLACE, + [_UNARY_NEGATIVE_FLOAT_INPLACE_r23] = _UNARY_NEGATIVE_FLOAT_INPLACE, + [_UNARY_NOT_r01] = _UNARY_NOT, + [_UNARY_NOT_r11] = _UNARY_NOT, + [_UNARY_NOT_r22] = _UNARY_NOT, + [_UNARY_NOT_r33] = _UNARY_NOT, + [_TO_BOOL_r11] = _TO_BOOL, + [_TO_BOOL_BOOL_r01] = _TO_BOOL_BOOL, + [_TO_BOOL_BOOL_r11] = _TO_BOOL_BOOL, + [_TO_BOOL_BOOL_r22] = _TO_BOOL_BOOL, + [_TO_BOOL_BOOL_r33] = _TO_BOOL_BOOL, + [_TO_BOOL_INT_r02] = _TO_BOOL_INT, + [_TO_BOOL_INT_r12] = _TO_BOOL_INT, + [_TO_BOOL_INT_r23] = _TO_BOOL_INT, + [_GUARD_NOS_LIST_r02] = _GUARD_NOS_LIST, + [_GUARD_NOS_LIST_r12] = _GUARD_NOS_LIST, + [_GUARD_NOS_LIST_r22] = _GUARD_NOS_LIST, + [_GUARD_NOS_LIST_r33] = _GUARD_NOS_LIST, + [_GUARD_TOS_LIST_r01] = _GUARD_TOS_LIST, + [_GUARD_TOS_LIST_r11] = _GUARD_TOS_LIST, + [_GUARD_TOS_LIST_r22] = _GUARD_TOS_LIST, + [_GUARD_TOS_LIST_r33] = _GUARD_TOS_LIST, + [_GUARD_TOS_SLICE_r01] = _GUARD_TOS_SLICE, + [_GUARD_TOS_SLICE_r11] = _GUARD_TOS_SLICE, + [_GUARD_TOS_SLICE_r22] = _GUARD_TOS_SLICE, + [_GUARD_TOS_SLICE_r33] = _GUARD_TOS_SLICE, + [_TO_BOOL_LIST_r02] = _TO_BOOL_LIST, + [_TO_BOOL_LIST_r12] = _TO_BOOL_LIST, + [_TO_BOOL_LIST_r23] = _TO_BOOL_LIST, + [_TO_BOOL_NONE_r01] = _TO_BOOL_NONE, + [_TO_BOOL_NONE_r11] = _TO_BOOL_NONE, + [_TO_BOOL_NONE_r22] = _TO_BOOL_NONE, + [_TO_BOOL_NONE_r33] = _TO_BOOL_NONE, + [_GUARD_NOS_COMPACT_ASCII_r02] = _GUARD_NOS_COMPACT_ASCII, + [_GUARD_NOS_COMPACT_ASCII_r12] = _GUARD_NOS_COMPACT_ASCII, + [_GUARD_NOS_COMPACT_ASCII_r22] = _GUARD_NOS_COMPACT_ASCII, + [_GUARD_NOS_COMPACT_ASCII_r33] = _GUARD_NOS_COMPACT_ASCII, + [_GUARD_NOS_UNICODE_r02] = _GUARD_NOS_UNICODE, + [_GUARD_NOS_UNICODE_r12] = _GUARD_NOS_UNICODE, + [_GUARD_NOS_UNICODE_r22] = _GUARD_NOS_UNICODE, + [_GUARD_NOS_UNICODE_r33] = _GUARD_NOS_UNICODE, + [_GUARD_TOS_UNICODE_r01] = _GUARD_TOS_UNICODE, + [_GUARD_TOS_UNICODE_r11] = _GUARD_TOS_UNICODE, + [_GUARD_TOS_UNICODE_r22] = _GUARD_TOS_UNICODE, + [_GUARD_TOS_UNICODE_r33] = _GUARD_TOS_UNICODE, + [_TO_BOOL_STR_r02] = _TO_BOOL_STR, + [_TO_BOOL_STR_r12] = _TO_BOOL_STR, + [_TO_BOOL_STR_r23] = _TO_BOOL_STR, + [_REPLACE_WITH_TRUE_r02] = _REPLACE_WITH_TRUE, + [_REPLACE_WITH_TRUE_r12] = _REPLACE_WITH_TRUE, + [_REPLACE_WITH_TRUE_r23] = _REPLACE_WITH_TRUE, + [_UNARY_INVERT_r12] = _UNARY_INVERT, + [_GUARD_NOS_INT_r02] = _GUARD_NOS_INT, + [_GUARD_NOS_INT_r12] = _GUARD_NOS_INT, + [_GUARD_NOS_INT_r22] = _GUARD_NOS_INT, + [_GUARD_NOS_INT_r33] = _GUARD_NOS_INT, + [_GUARD_TOS_INT_r01] = _GUARD_TOS_INT, + [_GUARD_TOS_INT_r11] = _GUARD_TOS_INT, + [_GUARD_TOS_INT_r22] = _GUARD_TOS_INT, + [_GUARD_TOS_INT_r33] = _GUARD_TOS_INT, + [_GUARD_NOS_OVERFLOWED_r02] = _GUARD_NOS_OVERFLOWED, + [_GUARD_NOS_OVERFLOWED_r12] = _GUARD_NOS_OVERFLOWED, + [_GUARD_NOS_OVERFLOWED_r22] = _GUARD_NOS_OVERFLOWED, + [_GUARD_NOS_OVERFLOWED_r33] = _GUARD_NOS_OVERFLOWED, + [_GUARD_TOS_OVERFLOWED_r01] = _GUARD_TOS_OVERFLOWED, + [_GUARD_TOS_OVERFLOWED_r11] = _GUARD_TOS_OVERFLOWED, + [_GUARD_TOS_OVERFLOWED_r22] = _GUARD_TOS_OVERFLOWED, + [_GUARD_TOS_OVERFLOWED_r33] = _GUARD_TOS_OVERFLOWED, + [_BINARY_OP_MULTIPLY_INT_r03] = _BINARY_OP_MULTIPLY_INT, + [_BINARY_OP_MULTIPLY_INT_r13] = _BINARY_OP_MULTIPLY_INT, + [_BINARY_OP_MULTIPLY_INT_r23] = _BINARY_OP_MULTIPLY_INT, + [_BINARY_OP_ADD_INT_r03] = _BINARY_OP_ADD_INT, + [_BINARY_OP_ADD_INT_r13] = _BINARY_OP_ADD_INT, + [_BINARY_OP_ADD_INT_r23] = _BINARY_OP_ADD_INT, + [_BINARY_OP_SUBTRACT_INT_r03] = _BINARY_OP_SUBTRACT_INT, + [_BINARY_OP_SUBTRACT_INT_r13] = _BINARY_OP_SUBTRACT_INT, + [_BINARY_OP_SUBTRACT_INT_r23] = _BINARY_OP_SUBTRACT_INT, + [_BINARY_OP_ADD_INT_INPLACE_r03] = _BINARY_OP_ADD_INT_INPLACE, + [_BINARY_OP_ADD_INT_INPLACE_r13] = _BINARY_OP_ADD_INT_INPLACE, + [_BINARY_OP_ADD_INT_INPLACE_r23] = _BINARY_OP_ADD_INT_INPLACE, + [_BINARY_OP_SUBTRACT_INT_INPLACE_r03] = _BINARY_OP_SUBTRACT_INT_INPLACE, + [_BINARY_OP_SUBTRACT_INT_INPLACE_r13] = _BINARY_OP_SUBTRACT_INT_INPLACE, + [_BINARY_OP_SUBTRACT_INT_INPLACE_r23] = _BINARY_OP_SUBTRACT_INT_INPLACE, + [_BINARY_OP_MULTIPLY_INT_INPLACE_r03] = _BINARY_OP_MULTIPLY_INT_INPLACE, + [_BINARY_OP_MULTIPLY_INT_INPLACE_r13] = _BINARY_OP_MULTIPLY_INT_INPLACE, + [_BINARY_OP_MULTIPLY_INT_INPLACE_r23] = _BINARY_OP_MULTIPLY_INT_INPLACE, + [_BINARY_OP_ADD_INT_INPLACE_RIGHT_r03] = _BINARY_OP_ADD_INT_INPLACE_RIGHT, + [_BINARY_OP_ADD_INT_INPLACE_RIGHT_r13] = _BINARY_OP_ADD_INT_INPLACE_RIGHT, + [_BINARY_OP_ADD_INT_INPLACE_RIGHT_r23] = _BINARY_OP_ADD_INT_INPLACE_RIGHT, + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r03] = _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT, + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r13] = _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT, + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r23] = _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT, + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r03] = _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT, + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r13] = _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT, + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r23] = _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT, + [_GUARD_NOS_FLOAT_r02] = _GUARD_NOS_FLOAT, + [_GUARD_NOS_FLOAT_r12] = _GUARD_NOS_FLOAT, + [_GUARD_NOS_FLOAT_r22] = _GUARD_NOS_FLOAT, + [_GUARD_NOS_FLOAT_r33] = _GUARD_NOS_FLOAT, + [_GUARD_TOS_FLOAT_r01] = _GUARD_TOS_FLOAT, + [_GUARD_TOS_FLOAT_r11] = _GUARD_TOS_FLOAT, + [_GUARD_TOS_FLOAT_r22] = _GUARD_TOS_FLOAT, + [_GUARD_TOS_FLOAT_r33] = _GUARD_TOS_FLOAT, + [_BINARY_OP_MULTIPLY_FLOAT_r03] = _BINARY_OP_MULTIPLY_FLOAT, + [_BINARY_OP_MULTIPLY_FLOAT_r13] = _BINARY_OP_MULTIPLY_FLOAT, + [_BINARY_OP_MULTIPLY_FLOAT_r23] = _BINARY_OP_MULTIPLY_FLOAT, + [_BINARY_OP_ADD_FLOAT_r03] = _BINARY_OP_ADD_FLOAT, + [_BINARY_OP_ADD_FLOAT_r13] = _BINARY_OP_ADD_FLOAT, + [_BINARY_OP_ADD_FLOAT_r23] = _BINARY_OP_ADD_FLOAT, + [_BINARY_OP_SUBTRACT_FLOAT_r03] = _BINARY_OP_SUBTRACT_FLOAT, + [_BINARY_OP_SUBTRACT_FLOAT_r13] = _BINARY_OP_SUBTRACT_FLOAT, + [_BINARY_OP_SUBTRACT_FLOAT_r23] = _BINARY_OP_SUBTRACT_FLOAT, + [_BINARY_OP_ADD_FLOAT_INPLACE_r03] = _BINARY_OP_ADD_FLOAT_INPLACE, + [_BINARY_OP_ADD_FLOAT_INPLACE_r13] = _BINARY_OP_ADD_FLOAT_INPLACE, + [_BINARY_OP_ADD_FLOAT_INPLACE_r23] = _BINARY_OP_ADD_FLOAT_INPLACE, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r03] = _BINARY_OP_SUBTRACT_FLOAT_INPLACE, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r13] = _BINARY_OP_SUBTRACT_FLOAT_INPLACE, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r23] = _BINARY_OP_SUBTRACT_FLOAT_INPLACE, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r03] = _BINARY_OP_MULTIPLY_FLOAT_INPLACE, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r13] = _BINARY_OP_MULTIPLY_FLOAT_INPLACE, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r23] = _BINARY_OP_MULTIPLY_FLOAT_INPLACE, + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r03] = _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r13] = _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r23] = _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r03] = _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r13] = _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r23] = _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r03] = _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r13] = _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r23] = _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_TRUEDIV_FLOAT_r23] = _BINARY_OP_TRUEDIV_FLOAT, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r03] = _BINARY_OP_TRUEDIV_FLOAT_INPLACE, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r13] = _BINARY_OP_TRUEDIV_FLOAT_INPLACE, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r23] = _BINARY_OP_TRUEDIV_FLOAT_INPLACE, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r03] = _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r13] = _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r23] = _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT, + [_BINARY_OP_ADD_UNICODE_r03] = _BINARY_OP_ADD_UNICODE, + [_BINARY_OP_ADD_UNICODE_r13] = _BINARY_OP_ADD_UNICODE, + [_BINARY_OP_ADD_UNICODE_r23] = _BINARY_OP_ADD_UNICODE, + [_BINARY_OP_INPLACE_ADD_UNICODE_r21] = _BINARY_OP_INPLACE_ADD_UNICODE, + [_GUARD_BINARY_OP_EXTEND_LHS_r02] = _GUARD_BINARY_OP_EXTEND_LHS, + [_GUARD_BINARY_OP_EXTEND_LHS_r12] = _GUARD_BINARY_OP_EXTEND_LHS, + [_GUARD_BINARY_OP_EXTEND_LHS_r22] = _GUARD_BINARY_OP_EXTEND_LHS, + [_GUARD_BINARY_OP_EXTEND_LHS_r33] = _GUARD_BINARY_OP_EXTEND_LHS, + [_GUARD_BINARY_OP_EXTEND_RHS_r02] = _GUARD_BINARY_OP_EXTEND_RHS, + [_GUARD_BINARY_OP_EXTEND_RHS_r12] = _GUARD_BINARY_OP_EXTEND_RHS, + [_GUARD_BINARY_OP_EXTEND_RHS_r22] = _GUARD_BINARY_OP_EXTEND_RHS, + [_GUARD_BINARY_OP_EXTEND_RHS_r33] = _GUARD_BINARY_OP_EXTEND_RHS, + [_GUARD_BINARY_OP_EXTEND_r22] = _GUARD_BINARY_OP_EXTEND, + [_BINARY_OP_EXTEND_r23] = _BINARY_OP_EXTEND, + [_BINARY_SLICE_r31] = _BINARY_SLICE, + [_STORE_SLICE_r30] = _STORE_SLICE, + [_BINARY_OP_SUBSCR_LIST_INT_r23] = _BINARY_OP_SUBSCR_LIST_INT, + [_BINARY_OP_SUBSCR_LIST_SLICE_r23] = _BINARY_OP_SUBSCR_LIST_SLICE, + [_BINARY_OP_SUBSCR_STR_INT_r23] = _BINARY_OP_SUBSCR_STR_INT, + [_BINARY_OP_SUBSCR_USTR_INT_r23] = _BINARY_OP_SUBSCR_USTR_INT, + [_GUARD_NOS_TUPLE_r02] = _GUARD_NOS_TUPLE, + [_GUARD_NOS_TUPLE_r12] = _GUARD_NOS_TUPLE, + [_GUARD_NOS_TUPLE_r22] = _GUARD_NOS_TUPLE, + [_GUARD_NOS_TUPLE_r33] = _GUARD_NOS_TUPLE, + [_GUARD_TOS_TUPLE_r01] = _GUARD_TOS_TUPLE, + [_GUARD_TOS_TUPLE_r11] = _GUARD_TOS_TUPLE, + [_GUARD_TOS_TUPLE_r22] = _GUARD_TOS_TUPLE, + [_GUARD_TOS_TUPLE_r33] = _GUARD_TOS_TUPLE, + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r02] = _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS, + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r12] = _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS, + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r22] = _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS, + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r33] = _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS, + [_BINARY_OP_SUBSCR_TUPLE_INT_r03] = _BINARY_OP_SUBSCR_TUPLE_INT, + [_BINARY_OP_SUBSCR_TUPLE_INT_r13] = _BINARY_OP_SUBSCR_TUPLE_INT, + [_BINARY_OP_SUBSCR_TUPLE_INT_r23] = _BINARY_OP_SUBSCR_TUPLE_INT, + [_GUARD_NOS_DICT_SUBSCRIPT_r02] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_SUBSCRIPT_r12] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_SUBSCRIPT_r22] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_SUBSCRIPT_r33] = _GUARD_NOS_DICT_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r03] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r13] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r23] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r33] = _GUARD_NOS_DICT_STORE_SUBSCRIPT, + [_GUARD_TOS_ANY_DICT_r01] = _GUARD_TOS_ANY_DICT, + [_GUARD_TOS_ANY_DICT_r11] = _GUARD_TOS_ANY_DICT, + [_GUARD_TOS_ANY_DICT_r22] = _GUARD_TOS_ANY_DICT, + [_GUARD_TOS_ANY_DICT_r33] = _GUARD_TOS_ANY_DICT, + [_GUARD_TOS_DICT_r01] = _GUARD_TOS_DICT, + [_GUARD_TOS_DICT_r11] = _GUARD_TOS_DICT, + [_GUARD_TOS_DICT_r22] = _GUARD_TOS_DICT, + [_GUARD_TOS_DICT_r33] = _GUARD_TOS_DICT, + [_GUARD_TOS_FROZENDICT_r01] = _GUARD_TOS_FROZENDICT, + [_GUARD_TOS_FROZENDICT_r11] = _GUARD_TOS_FROZENDICT, + [_GUARD_TOS_FROZENDICT_r22] = _GUARD_TOS_FROZENDICT, + [_GUARD_TOS_FROZENDICT_r33] = _GUARD_TOS_FROZENDICT, + [_BINARY_OP_SUBSCR_DICT_KNOWN_HASH_r23] = _BINARY_OP_SUBSCR_DICT_KNOWN_HASH, + [_BINARY_OP_SUBSCR_DICT_r23] = _BINARY_OP_SUBSCR_DICT, + [_BINARY_OP_SUBSCR_CHECK_FUNC_r23] = _BINARY_OP_SUBSCR_CHECK_FUNC, + [_BINARY_OP_SUBSCR_INIT_CALL_r01] = _BINARY_OP_SUBSCR_INIT_CALL, + [_BINARY_OP_SUBSCR_INIT_CALL_r11] = _BINARY_OP_SUBSCR_INIT_CALL, + [_BINARY_OP_SUBSCR_INIT_CALL_r21] = _BINARY_OP_SUBSCR_INIT_CALL, + [_BINARY_OP_SUBSCR_INIT_CALL_r31] = _BINARY_OP_SUBSCR_INIT_CALL, + [_LIST_APPEND_r10] = _LIST_APPEND, + [_SET_ADD_r10] = _SET_ADD, + [_STORE_SUBSCR_r30] = _STORE_SUBSCR, + [_STORE_SUBSCR_LIST_INT_r32] = _STORE_SUBSCR_LIST_INT, + [_STORE_SUBSCR_DICT_r31] = _STORE_SUBSCR_DICT, + [_STORE_SUBSCR_DICT_KNOWN_HASH_r31] = _STORE_SUBSCR_DICT_KNOWN_HASH, + [_DELETE_SUBSCR_r20] = _DELETE_SUBSCR, + [_CALL_INTRINSIC_1_r12] = _CALL_INTRINSIC_1, + [_CALL_INTRINSIC_2_r23] = _CALL_INTRINSIC_2, + [_MAKE_HEAP_SAFE_r01] = _MAKE_HEAP_SAFE, + [_MAKE_HEAP_SAFE_r11] = _MAKE_HEAP_SAFE, + [_MAKE_HEAP_SAFE_r22] = _MAKE_HEAP_SAFE, + [_MAKE_HEAP_SAFE_r33] = _MAKE_HEAP_SAFE, + [_RETURN_VALUE_r11] = _RETURN_VALUE, + [_GET_AITER_r11] = _GET_AITER, + [_GET_ANEXT_r12] = _GET_ANEXT, + [_GET_AWAITABLE_r11] = _GET_AWAITABLE, + [_SEND_GEN_FRAME_r33] = _SEND_GEN_FRAME, + [_GUARD_TOS_IS_NONE_r01] = _GUARD_TOS_IS_NONE, + [_GUARD_TOS_IS_NONE_r11] = _GUARD_TOS_IS_NONE, + [_GUARD_TOS_IS_NONE_r22] = _GUARD_TOS_IS_NONE, + [_GUARD_TOS_IS_NONE_r33] = _GUARD_TOS_IS_NONE, + [_GUARD_NOS_NOT_NULL_r02] = _GUARD_NOS_NOT_NULL, + [_GUARD_NOS_NOT_NULL_r12] = _GUARD_NOS_NOT_NULL, + [_GUARD_NOS_NOT_NULL_r22] = _GUARD_NOS_NOT_NULL, + [_GUARD_NOS_NOT_NULL_r33] = _GUARD_NOS_NOT_NULL, + [_SEND_VIRTUAL_TIER_TWO_r03] = _SEND_VIRTUAL_TIER_TWO, + [_SEND_VIRTUAL_TIER_TWO_r13] = _SEND_VIRTUAL_TIER_TWO, + [_SEND_VIRTUAL_TIER_TWO_r23] = _SEND_VIRTUAL_TIER_TWO, + [_SEND_VIRTUAL_TIER_TWO_r33] = _SEND_VIRTUAL_TIER_TWO, + [_GUARD_3OS_ASYNC_GEN_ASEND_r03] = _GUARD_3OS_ASYNC_GEN_ASEND, + [_GUARD_3OS_ASYNC_GEN_ASEND_r13] = _GUARD_3OS_ASYNC_GEN_ASEND, + [_GUARD_3OS_ASYNC_GEN_ASEND_r23] = _GUARD_3OS_ASYNC_GEN_ASEND, + [_GUARD_3OS_ASYNC_GEN_ASEND_r33] = _GUARD_3OS_ASYNC_GEN_ASEND, + [_SEND_ASYNC_GEN_TIER_TWO_r33] = _SEND_ASYNC_GEN_TIER_TWO, + [_YIELD_VALUE_r11] = _YIELD_VALUE, + [_POP_EXCEPT_r10] = _POP_EXCEPT, + [_LOAD_COMMON_CONSTANT_r01] = _LOAD_COMMON_CONSTANT, + [_LOAD_COMMON_CONSTANT_r12] = _LOAD_COMMON_CONSTANT, + [_LOAD_COMMON_CONSTANT_r23] = _LOAD_COMMON_CONSTANT, + [_LOAD_BUILD_CLASS_r01] = _LOAD_BUILD_CLASS, + [_STORE_NAME_r10] = _STORE_NAME, + [_DELETE_NAME_r00] = _DELETE_NAME, + [_UNPACK_SEQUENCE_r10] = _UNPACK_SEQUENCE, + [_UNPACK_SEQUENCE_TWO_TUPLE_r12] = _UNPACK_SEQUENCE_TWO_TUPLE, + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r02] = _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE, + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r12] = _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE, + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r23] = _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE, + [_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r03] = _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE, + [_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r13] = _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE, + [_UNPACK_SEQUENCE_TUPLE_r10] = _UNPACK_SEQUENCE_TUPLE, + [_UNPACK_SEQUENCE_UNIQUE_TUPLE_r10] = _UNPACK_SEQUENCE_UNIQUE_TUPLE, + [_UNPACK_SEQUENCE_LIST_r10] = _UNPACK_SEQUENCE_LIST, + [_UNPACK_EX_r10] = _UNPACK_EX, + [_STORE_ATTR_r20] = _STORE_ATTR, + [_DELETE_ATTR_r10] = _DELETE_ATTR, + [_STORE_GLOBAL_r10] = _STORE_GLOBAL, + [_DELETE_GLOBAL_r00] = _DELETE_GLOBAL, + [_LOAD_LOCALS_r01] = _LOAD_LOCALS, + [_LOAD_LOCALS_r12] = _LOAD_LOCALS, + [_LOAD_LOCALS_r23] = _LOAD_LOCALS, + [_LOAD_FROM_DICT_OR_GLOBALS_r11] = _LOAD_FROM_DICT_OR_GLOBALS, + [_LOAD_NAME_r01] = _LOAD_NAME, + [_LOAD_GLOBAL_r00] = _LOAD_GLOBAL, + [_PUSH_NULL_CONDITIONAL_r00] = _PUSH_NULL_CONDITIONAL, + [_GUARD_GLOBALS_VERSION_r00] = _GUARD_GLOBALS_VERSION, + [_GUARD_GLOBALS_VERSION_r11] = _GUARD_GLOBALS_VERSION, + [_GUARD_GLOBALS_VERSION_r22] = _GUARD_GLOBALS_VERSION, + [_GUARD_GLOBALS_VERSION_r33] = _GUARD_GLOBALS_VERSION, + [_LOAD_GLOBAL_MODULE_r01] = _LOAD_GLOBAL_MODULE, + [_LOAD_GLOBAL_BUILTINS_r01] = _LOAD_GLOBAL_BUILTINS, + [_DELETE_FAST_r00] = _DELETE_FAST, + [_MAKE_CELL_r00] = _MAKE_CELL, + [_DELETE_DEREF_r00] = _DELETE_DEREF, + [_LOAD_FROM_DICT_OR_DEREF_r11] = _LOAD_FROM_DICT_OR_DEREF, + [_LOAD_DEREF_r01] = _LOAD_DEREF, + [_STORE_DEREF_r10] = _STORE_DEREF, + [_COPY_FREE_VARS_r00] = _COPY_FREE_VARS, + [_COPY_FREE_VARS_r11] = _COPY_FREE_VARS, + [_COPY_FREE_VARS_r22] = _COPY_FREE_VARS, + [_COPY_FREE_VARS_r33] = _COPY_FREE_VARS, + [_BUILD_STRING_r01] = _BUILD_STRING, + [_BUILD_INTERPOLATION_r01] = _BUILD_INTERPOLATION, + [_BUILD_TEMPLATE_r21] = _BUILD_TEMPLATE, + [_BUILD_TUPLE_r01] = _BUILD_TUPLE, + [_BUILD_LIST_r01] = _BUILD_LIST, + [_LIST_EXTEND_r11] = _LIST_EXTEND, + [_SET_UPDATE_r11] = _SET_UPDATE, + [_BUILD_SET_r01] = _BUILD_SET, + [_BUILD_MAP_r01] = _BUILD_MAP, + [_SETUP_ANNOTATIONS_r00] = _SETUP_ANNOTATIONS, + [_DICT_UPDATE_r11] = _DICT_UPDATE, + [_DICT_MERGE_r11] = _DICT_MERGE, + [_MAP_ADD_r20] = _MAP_ADD, + [_LOAD_SUPER_ATTR_ATTR_r31] = _LOAD_SUPER_ATTR_ATTR, + [_GUARD_NOS_TYPE_VERSION_r02] = _GUARD_NOS_TYPE_VERSION, + [_GUARD_NOS_TYPE_VERSION_r12] = _GUARD_NOS_TYPE_VERSION, + [_GUARD_NOS_TYPE_VERSION_r22] = _GUARD_NOS_TYPE_VERSION, + [_GUARD_NOS_TYPE_VERSION_r33] = _GUARD_NOS_TYPE_VERSION, + [_GUARD_LOAD_SUPER_ATTR_METHOD_r03] = _GUARD_LOAD_SUPER_ATTR_METHOD, + [_GUARD_LOAD_SUPER_ATTR_METHOD_r13] = _GUARD_LOAD_SUPER_ATTR_METHOD, + [_GUARD_LOAD_SUPER_ATTR_METHOD_r23] = _GUARD_LOAD_SUPER_ATTR_METHOD, + [_GUARD_LOAD_SUPER_ATTR_METHOD_r33] = _GUARD_LOAD_SUPER_ATTR_METHOD, + [_LOAD_SUPER_ATTR_METHOD_r32] = _LOAD_SUPER_ATTR_METHOD, + [_LOAD_ATTR_r10] = _LOAD_ATTR, + [_GUARD_TYPE_VERSION_r01] = _GUARD_TYPE_VERSION, + [_GUARD_TYPE_VERSION_r11] = _GUARD_TYPE_VERSION, + [_GUARD_TYPE_VERSION_r22] = _GUARD_TYPE_VERSION, + [_GUARD_TYPE_VERSION_r33] = _GUARD_TYPE_VERSION, + [_GUARD_TYPE_VERSION_LOCKED_r01] = _GUARD_TYPE_VERSION_LOCKED, + [_GUARD_TYPE_VERSION_LOCKED_r11] = _GUARD_TYPE_VERSION_LOCKED, + [_GUARD_TYPE_VERSION_LOCKED_r22] = _GUARD_TYPE_VERSION_LOCKED, + [_GUARD_TYPE_VERSION_LOCKED_r33] = _GUARD_TYPE_VERSION_LOCKED, + [_GUARD_TYPE_r01] = _GUARD_TYPE, + [_GUARD_TYPE_r11] = _GUARD_TYPE, + [_GUARD_TYPE_r22] = _GUARD_TYPE, + [_GUARD_TYPE_r33] = _GUARD_TYPE, + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r01] = _CHECK_MANAGED_OBJECT_HAS_VALUES, + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r11] = _CHECK_MANAGED_OBJECT_HAS_VALUES, + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r22] = _CHECK_MANAGED_OBJECT_HAS_VALUES, + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r33] = _CHECK_MANAGED_OBJECT_HAS_VALUES, + [_LOAD_ATTR_INSTANCE_VALUE_r02] = _LOAD_ATTR_INSTANCE_VALUE, + [_LOAD_ATTR_INSTANCE_VALUE_r12] = _LOAD_ATTR_INSTANCE_VALUE, + [_LOAD_ATTR_INSTANCE_VALUE_r23] = _LOAD_ATTR_INSTANCE_VALUE, + [_LOAD_ATTR_MODULE_r12] = _LOAD_ATTR_MODULE, + [_LOAD_ATTR_WITH_HINT_r12] = _LOAD_ATTR_WITH_HINT, + [_LOAD_ATTR_SLOT_r02] = _LOAD_ATTR_SLOT, + [_LOAD_ATTR_SLOT_r12] = _LOAD_ATTR_SLOT, + [_LOAD_ATTR_SLOT_r23] = _LOAD_ATTR_SLOT, + [_CHECK_ATTR_CLASS_r01] = _CHECK_ATTR_CLASS, + [_CHECK_ATTR_CLASS_r11] = _CHECK_ATTR_CLASS, + [_CHECK_ATTR_CLASS_r22] = _CHECK_ATTR_CLASS, + [_CHECK_ATTR_CLASS_r33] = _CHECK_ATTR_CLASS, + [_LOAD_ATTR_CLASS_r11] = _LOAD_ATTR_CLASS, + [_LOAD_ATTR_PROPERTY_FRAME_r01] = _LOAD_ATTR_PROPERTY_FRAME, + [_LOAD_ATTR_PROPERTY_FRAME_r11] = _LOAD_ATTR_PROPERTY_FRAME, + [_LOAD_ATTR_PROPERTY_FRAME_r22] = _LOAD_ATTR_PROPERTY_FRAME, + [_LOAD_ATTR_PROPERTY_FRAME_r33] = _LOAD_ATTR_PROPERTY_FRAME, + [_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME_r11] = _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME, + [_GUARD_DORV_NO_DICT_r01] = _GUARD_DORV_NO_DICT, + [_GUARD_DORV_NO_DICT_r11] = _GUARD_DORV_NO_DICT, + [_GUARD_DORV_NO_DICT_r22] = _GUARD_DORV_NO_DICT, + [_GUARD_DORV_NO_DICT_r33] = _GUARD_DORV_NO_DICT, + [_STORE_ATTR_INSTANCE_VALUE_r21] = _STORE_ATTR_INSTANCE_VALUE, + [_LOCK_OBJECT_r01] = _LOCK_OBJECT, + [_LOCK_OBJECT_r11] = _LOCK_OBJECT, + [_LOCK_OBJECT_r22] = _LOCK_OBJECT, + [_LOCK_OBJECT_r33] = _LOCK_OBJECT, + [_STORE_ATTR_WITH_HINT_r21] = _STORE_ATTR_WITH_HINT, + [_STORE_ATTR_SLOT_r21] = _STORE_ATTR_SLOT, + [_COMPARE_OP_r21] = _COMPARE_OP, + [_COMPARE_OP_FLOAT_r03] = _COMPARE_OP_FLOAT, + [_COMPARE_OP_FLOAT_r13] = _COMPARE_OP_FLOAT, + [_COMPARE_OP_FLOAT_r23] = _COMPARE_OP_FLOAT, + [_COMPARE_OP_INT_r23] = _COMPARE_OP_INT, + [_COMPARE_OP_STR_r23] = _COMPARE_OP_STR, + [_IS_OP_r03] = _IS_OP, + [_IS_OP_r13] = _IS_OP, + [_IS_OP_r23] = _IS_OP, + [_CONTAINS_OP_r23] = _CONTAINS_OP, + [_GUARD_TOS_ANY_SET_r01] = _GUARD_TOS_ANY_SET, + [_GUARD_TOS_ANY_SET_r11] = _GUARD_TOS_ANY_SET, + [_GUARD_TOS_ANY_SET_r22] = _GUARD_TOS_ANY_SET, + [_GUARD_TOS_ANY_SET_r33] = _GUARD_TOS_ANY_SET, + [_GUARD_TOS_SET_r01] = _GUARD_TOS_SET, + [_GUARD_TOS_SET_r11] = _GUARD_TOS_SET, + [_GUARD_TOS_SET_r22] = _GUARD_TOS_SET, + [_GUARD_TOS_SET_r33] = _GUARD_TOS_SET, + [_GUARD_TOS_FROZENSET_r01] = _GUARD_TOS_FROZENSET, + [_GUARD_TOS_FROZENSET_r11] = _GUARD_TOS_FROZENSET, + [_GUARD_TOS_FROZENSET_r22] = _GUARD_TOS_FROZENSET, + [_GUARD_TOS_FROZENSET_r33] = _GUARD_TOS_FROZENSET, + [_CONTAINS_OP_SET_r23] = _CONTAINS_OP_SET, + [_CONTAINS_OP_DICT_r23] = _CONTAINS_OP_DICT, + [_CHECK_EG_MATCH_r22] = _CHECK_EG_MATCH, + [_CHECK_EXC_MATCH_r22] = _CHECK_EXC_MATCH, + [_IMPORT_NAME_r21] = _IMPORT_NAME, + [_IMPORT_FROM_r12] = _IMPORT_FROM, + [_IS_NONE_r11] = _IS_NONE, + [_GET_LEN_r12] = _GET_LEN, + [_MATCH_CLASS_r33] = _MATCH_CLASS, + [_MATCH_MAPPING_r02] = _MATCH_MAPPING, + [_MATCH_MAPPING_r12] = _MATCH_MAPPING, + [_MATCH_MAPPING_r23] = _MATCH_MAPPING, + [_MATCH_SEQUENCE_r02] = _MATCH_SEQUENCE, + [_MATCH_SEQUENCE_r12] = _MATCH_SEQUENCE, + [_MATCH_SEQUENCE_r23] = _MATCH_SEQUENCE, + [_MATCH_KEYS_r23] = _MATCH_KEYS, + [_GET_ITER_r12] = _GET_ITER, + [_GUARD_ITERATOR_r01] = _GUARD_ITERATOR, + [_GUARD_ITERATOR_r11] = _GUARD_ITERATOR, + [_GUARD_ITERATOR_r22] = _GUARD_ITERATOR, + [_GUARD_ITERATOR_r33] = _GUARD_ITERATOR, + [_GUARD_ITER_VIRTUAL_r01] = _GUARD_ITER_VIRTUAL, + [_GUARD_ITER_VIRTUAL_r11] = _GUARD_ITER_VIRTUAL, + [_GUARD_ITER_VIRTUAL_r22] = _GUARD_ITER_VIRTUAL, + [_GUARD_ITER_VIRTUAL_r33] = _GUARD_ITER_VIRTUAL, + [_PUSH_TAGGED_ZERO_r01] = _PUSH_TAGGED_ZERO, + [_PUSH_TAGGED_ZERO_r12] = _PUSH_TAGGED_ZERO, + [_PUSH_TAGGED_ZERO_r23] = _PUSH_TAGGED_ZERO, + [_GET_ITER_TRAD_r12] = _GET_ITER_TRAD, + [_FOR_ITER_TIER_TWO_r23] = _FOR_ITER_TIER_TWO, + [_GUARD_TYPE_ITER_r02] = _GUARD_TYPE_ITER, + [_GUARD_TYPE_ITER_r12] = _GUARD_TYPE_ITER, + [_GUARD_TYPE_ITER_r22] = _GUARD_TYPE_ITER, + [_GUARD_TYPE_ITER_r33] = _GUARD_TYPE_ITER, + [_ITER_NEXT_INLINE_r23] = _ITER_NEXT_INLINE, + [_GUARD_NOS_ITER_VIRTUAL_r02] = _GUARD_NOS_ITER_VIRTUAL, + [_GUARD_NOS_ITER_VIRTUAL_r12] = _GUARD_NOS_ITER_VIRTUAL, + [_GUARD_NOS_ITER_VIRTUAL_r22] = _GUARD_NOS_ITER_VIRTUAL, + [_GUARD_NOS_ITER_VIRTUAL_r33] = _GUARD_NOS_ITER_VIRTUAL, + [_GUARD_TOS_NOT_NULL_r01] = _GUARD_TOS_NOT_NULL, + [_GUARD_TOS_NOT_NULL_r11] = _GUARD_TOS_NOT_NULL, + [_GUARD_TOS_NOT_NULL_r22] = _GUARD_TOS_NOT_NULL, + [_GUARD_TOS_NOT_NULL_r33] = _GUARD_TOS_NOT_NULL, + [_FOR_ITER_VIRTUAL_TIER_TWO_r23] = _FOR_ITER_VIRTUAL_TIER_TWO, + [_ITER_CHECK_LIST_r02] = _ITER_CHECK_LIST, + [_ITER_CHECK_LIST_r12] = _ITER_CHECK_LIST, + [_ITER_CHECK_LIST_r22] = _ITER_CHECK_LIST, + [_ITER_CHECK_LIST_r33] = _ITER_CHECK_LIST, + [_GUARD_NOT_EXHAUSTED_LIST_r02] = _GUARD_NOT_EXHAUSTED_LIST, + [_GUARD_NOT_EXHAUSTED_LIST_r12] = _GUARD_NOT_EXHAUSTED_LIST, + [_GUARD_NOT_EXHAUSTED_LIST_r22] = _GUARD_NOT_EXHAUSTED_LIST, + [_GUARD_NOT_EXHAUSTED_LIST_r33] = _GUARD_NOT_EXHAUSTED_LIST, + [_ITER_NEXT_LIST_TIER_TWO_r23] = _ITER_NEXT_LIST_TIER_TWO, + [_ITER_CHECK_TUPLE_r02] = _ITER_CHECK_TUPLE, + [_ITER_CHECK_TUPLE_r12] = _ITER_CHECK_TUPLE, + [_ITER_CHECK_TUPLE_r22] = _ITER_CHECK_TUPLE, + [_ITER_CHECK_TUPLE_r33] = _ITER_CHECK_TUPLE, + [_GUARD_NOT_EXHAUSTED_TUPLE_r02] = _GUARD_NOT_EXHAUSTED_TUPLE, + [_GUARD_NOT_EXHAUSTED_TUPLE_r12] = _GUARD_NOT_EXHAUSTED_TUPLE, + [_GUARD_NOT_EXHAUSTED_TUPLE_r22] = _GUARD_NOT_EXHAUSTED_TUPLE, + [_GUARD_NOT_EXHAUSTED_TUPLE_r33] = _GUARD_NOT_EXHAUSTED_TUPLE, + [_ITER_NEXT_TUPLE_r03] = _ITER_NEXT_TUPLE, + [_ITER_NEXT_TUPLE_r13] = _ITER_NEXT_TUPLE, + [_ITER_NEXT_TUPLE_r23] = _ITER_NEXT_TUPLE, + [_ITER_CHECK_RANGE_r02] = _ITER_CHECK_RANGE, + [_ITER_CHECK_RANGE_r12] = _ITER_CHECK_RANGE, + [_ITER_CHECK_RANGE_r22] = _ITER_CHECK_RANGE, + [_ITER_CHECK_RANGE_r33] = _ITER_CHECK_RANGE, + [_GUARD_NOT_EXHAUSTED_RANGE_r02] = _GUARD_NOT_EXHAUSTED_RANGE, + [_GUARD_NOT_EXHAUSTED_RANGE_r12] = _GUARD_NOT_EXHAUSTED_RANGE, + [_GUARD_NOT_EXHAUSTED_RANGE_r22] = _GUARD_NOT_EXHAUSTED_RANGE, + [_GUARD_NOT_EXHAUSTED_RANGE_r33] = _GUARD_NOT_EXHAUSTED_RANGE, + [_ITER_NEXT_RANGE_r03] = _ITER_NEXT_RANGE, + [_ITER_NEXT_RANGE_r13] = _ITER_NEXT_RANGE, + [_ITER_NEXT_RANGE_r23] = _ITER_NEXT_RANGE, + [_FOR_ITER_GEN_FRAME_r03] = _FOR_ITER_GEN_FRAME, + [_FOR_ITER_GEN_FRAME_r13] = _FOR_ITER_GEN_FRAME, + [_FOR_ITER_GEN_FRAME_r23] = _FOR_ITER_GEN_FRAME, + [_INSERT_NULL_r10] = _INSERT_NULL, + [_LOAD_SPECIAL_r00] = _LOAD_SPECIAL, + [_WITH_EXCEPT_START_r33] = _WITH_EXCEPT_START, + [_PUSH_EXC_INFO_r02] = _PUSH_EXC_INFO, + [_PUSH_EXC_INFO_r12] = _PUSH_EXC_INFO, + [_PUSH_EXC_INFO_r23] = _PUSH_EXC_INFO, + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r01] = _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r11] = _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r22] = _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r33] = _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT, + [_GUARD_KEYS_VERSION_r01] = _GUARD_KEYS_VERSION, + [_GUARD_KEYS_VERSION_r11] = _GUARD_KEYS_VERSION, + [_GUARD_KEYS_VERSION_r22] = _GUARD_KEYS_VERSION, + [_GUARD_KEYS_VERSION_r33] = _GUARD_KEYS_VERSION, + [_LOAD_ATTR_METHOD_WITH_VALUES_r02] = _LOAD_ATTR_METHOD_WITH_VALUES, + [_LOAD_ATTR_METHOD_WITH_VALUES_r12] = _LOAD_ATTR_METHOD_WITH_VALUES, + [_LOAD_ATTR_METHOD_WITH_VALUES_r23] = _LOAD_ATTR_METHOD_WITH_VALUES, + [_LOAD_ATTR_METHOD_NO_DICT_r02] = _LOAD_ATTR_METHOD_NO_DICT, + [_LOAD_ATTR_METHOD_NO_DICT_r12] = _LOAD_ATTR_METHOD_NO_DICT, + [_LOAD_ATTR_METHOD_NO_DICT_r23] = _LOAD_ATTR_METHOD_NO_DICT, + [_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES_r11] = _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES, + [_LOAD_ATTR_NONDESCRIPTOR_NO_DICT_r11] = _LOAD_ATTR_NONDESCRIPTOR_NO_DICT, + [_CHECK_ATTR_METHOD_LAZY_DICT_r01] = _CHECK_ATTR_METHOD_LAZY_DICT, + [_CHECK_ATTR_METHOD_LAZY_DICT_r11] = _CHECK_ATTR_METHOD_LAZY_DICT, + [_CHECK_ATTR_METHOD_LAZY_DICT_r22] = _CHECK_ATTR_METHOD_LAZY_DICT, + [_CHECK_ATTR_METHOD_LAZY_DICT_r33] = _CHECK_ATTR_METHOD_LAZY_DICT, + [_LOAD_ATTR_METHOD_LAZY_DICT_r02] = _LOAD_ATTR_METHOD_LAZY_DICT, + [_LOAD_ATTR_METHOD_LAZY_DICT_r12] = _LOAD_ATTR_METHOD_LAZY_DICT, + [_LOAD_ATTR_METHOD_LAZY_DICT_r23] = _LOAD_ATTR_METHOD_LAZY_DICT, + [_MAYBE_EXPAND_METHOD_r00] = _MAYBE_EXPAND_METHOD, + [_PY_FRAME_GENERAL_r01] = _PY_FRAME_GENERAL, + [_CHECK_FUNCTION_VERSION_r00] = _CHECK_FUNCTION_VERSION, + [_CHECK_FUNCTION_VERSION_INLINE_r00] = _CHECK_FUNCTION_VERSION_INLINE, + [_CHECK_FUNCTION_VERSION_INLINE_r11] = _CHECK_FUNCTION_VERSION_INLINE, + [_CHECK_FUNCTION_VERSION_INLINE_r22] = _CHECK_FUNCTION_VERSION_INLINE, + [_CHECK_FUNCTION_VERSION_INLINE_r33] = _CHECK_FUNCTION_VERSION_INLINE, + [_CHECK_METHOD_VERSION_r00] = _CHECK_METHOD_VERSION, + [_EXPAND_METHOD_r00] = _EXPAND_METHOD, + [_CHECK_IS_NOT_PY_CALLABLE_r00] = _CHECK_IS_NOT_PY_CALLABLE, + [_CALL_NON_PY_GENERAL_r01] = _CALL_NON_PY_GENERAL, + [_CHECK_CALL_BOUND_METHOD_EXACT_ARGS_r00] = _CHECK_CALL_BOUND_METHOD_EXACT_ARGS, + [_INIT_CALL_BOUND_METHOD_EXACT_ARGS_r00] = _INIT_CALL_BOUND_METHOD_EXACT_ARGS, + [_CHECK_PEP_523_r00] = _CHECK_PEP_523, + [_CHECK_PEP_523_r11] = _CHECK_PEP_523, + [_CHECK_PEP_523_r22] = _CHECK_PEP_523, + [_CHECK_PEP_523_r33] = _CHECK_PEP_523, + [_CHECK_FUNCTION_EXACT_ARGS_r00] = _CHECK_FUNCTION_EXACT_ARGS, + [_CHECK_STACK_SPACE_r00] = _CHECK_STACK_SPACE, + [_CHECK_RECURSION_REMAINING_r00] = _CHECK_RECURSION_REMAINING, + [_CHECK_RECURSION_REMAINING_r11] = _CHECK_RECURSION_REMAINING, + [_CHECK_RECURSION_REMAINING_r22] = _CHECK_RECURSION_REMAINING, + [_CHECK_RECURSION_REMAINING_r33] = _CHECK_RECURSION_REMAINING, + [_INIT_CALL_PY_EXACT_ARGS_0_r01] = _INIT_CALL_PY_EXACT_ARGS_0, + [_INIT_CALL_PY_EXACT_ARGS_1_r01] = _INIT_CALL_PY_EXACT_ARGS_1, + [_INIT_CALL_PY_EXACT_ARGS_2_r01] = _INIT_CALL_PY_EXACT_ARGS_2, + [_INIT_CALL_PY_EXACT_ARGS_3_r01] = _INIT_CALL_PY_EXACT_ARGS_3, + [_INIT_CALL_PY_EXACT_ARGS_4_r01] = _INIT_CALL_PY_EXACT_ARGS_4, + [_INIT_CALL_PY_EXACT_ARGS_r01] = _INIT_CALL_PY_EXACT_ARGS, + [_PUSH_FRAME_r10] = _PUSH_FRAME, + [_GUARD_NOS_NULL_r02] = _GUARD_NOS_NULL, + [_GUARD_NOS_NULL_r12] = _GUARD_NOS_NULL, + [_GUARD_NOS_NULL_r22] = _GUARD_NOS_NULL, + [_GUARD_NOS_NULL_r33] = _GUARD_NOS_NULL, + [_GUARD_THIRD_NULL_r03] = _GUARD_THIRD_NULL, + [_GUARD_THIRD_NULL_r13] = _GUARD_THIRD_NULL, + [_GUARD_THIRD_NULL_r23] = _GUARD_THIRD_NULL, + [_GUARD_THIRD_NULL_r33] = _GUARD_THIRD_NULL, + [_GUARD_CALLABLE_TYPE_1_r03] = _GUARD_CALLABLE_TYPE_1, + [_GUARD_CALLABLE_TYPE_1_r13] = _GUARD_CALLABLE_TYPE_1, + [_GUARD_CALLABLE_TYPE_1_r23] = _GUARD_CALLABLE_TYPE_1, + [_GUARD_CALLABLE_TYPE_1_r33] = _GUARD_CALLABLE_TYPE_1, + [_CALL_TYPE_1_r02] = _CALL_TYPE_1, + [_CALL_TYPE_1_r12] = _CALL_TYPE_1, + [_CALL_TYPE_1_r22] = _CALL_TYPE_1, + [_CALL_TYPE_1_r32] = _CALL_TYPE_1, + [_GUARD_CALLABLE_STR_1_r03] = _GUARD_CALLABLE_STR_1, + [_GUARD_CALLABLE_STR_1_r13] = _GUARD_CALLABLE_STR_1, + [_GUARD_CALLABLE_STR_1_r23] = _GUARD_CALLABLE_STR_1, + [_GUARD_CALLABLE_STR_1_r33] = _GUARD_CALLABLE_STR_1, + [_CALL_STR_1_r32] = _CALL_STR_1, + [_GUARD_CALLABLE_TUPLE_1_r03] = _GUARD_CALLABLE_TUPLE_1, + [_GUARD_CALLABLE_TUPLE_1_r13] = _GUARD_CALLABLE_TUPLE_1, + [_GUARD_CALLABLE_TUPLE_1_r23] = _GUARD_CALLABLE_TUPLE_1, + [_GUARD_CALLABLE_TUPLE_1_r33] = _GUARD_CALLABLE_TUPLE_1, + [_CALL_TUPLE_1_r32] = _CALL_TUPLE_1, + [_CHECK_OBJECT_r00] = _CHECK_OBJECT, + [_ALLOCATE_OBJECT_r00] = _ALLOCATE_OBJECT, + [_CREATE_INIT_FRAME_r01] = _CREATE_INIT_FRAME, + [_EXIT_INIT_CHECK_r10] = _EXIT_INIT_CHECK, + [_GUARD_CALLABLE_BUILTIN_CLASS_r00] = _GUARD_CALLABLE_BUILTIN_CLASS, + [_CALL_BUILTIN_CLASS_r00] = _CALL_BUILTIN_CLASS, + [_GUARD_CALLABLE_BUILTIN_O_r00] = _GUARD_CALLABLE_BUILTIN_O, + [_CALL_BUILTIN_O_r03] = _CALL_BUILTIN_O, + [_GUARD_CALLABLE_BUILTIN_FAST_r00] = _GUARD_CALLABLE_BUILTIN_FAST, + [_CALL_BUILTIN_FAST_r00] = _CALL_BUILTIN_FAST, + [_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS_r00] = _GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS, + [_CALL_BUILTIN_FAST_WITH_KEYWORDS_r00] = _CALL_BUILTIN_FAST_WITH_KEYWORDS, + [_GUARD_CALLABLE_LEN_r03] = _GUARD_CALLABLE_LEN, + [_GUARD_CALLABLE_LEN_r13] = _GUARD_CALLABLE_LEN, + [_GUARD_CALLABLE_LEN_r23] = _GUARD_CALLABLE_LEN, + [_GUARD_CALLABLE_LEN_r33] = _GUARD_CALLABLE_LEN, + [_CALL_LEN_r33] = _CALL_LEN, + [_GUARD_CALLABLE_ISINSTANCE_r03] = _GUARD_CALLABLE_ISINSTANCE, + [_GUARD_CALLABLE_ISINSTANCE_r13] = _GUARD_CALLABLE_ISINSTANCE, + [_GUARD_CALLABLE_ISINSTANCE_r23] = _GUARD_CALLABLE_ISINSTANCE, + [_GUARD_CALLABLE_ISINSTANCE_r33] = _GUARD_CALLABLE_ISINSTANCE, + [_CALL_ISINSTANCE_r31] = _CALL_ISINSTANCE, + [_GUARD_CALLABLE_LIST_APPEND_r03] = _GUARD_CALLABLE_LIST_APPEND, + [_GUARD_CALLABLE_LIST_APPEND_r13] = _GUARD_CALLABLE_LIST_APPEND, + [_GUARD_CALLABLE_LIST_APPEND_r23] = _GUARD_CALLABLE_LIST_APPEND, + [_GUARD_CALLABLE_LIST_APPEND_r33] = _GUARD_CALLABLE_LIST_APPEND, + [_CALL_LIST_APPEND_r03] = _CALL_LIST_APPEND, + [_CALL_LIST_APPEND_r13] = _CALL_LIST_APPEND, + [_CALL_LIST_APPEND_r23] = _CALL_LIST_APPEND, + [_CALL_LIST_APPEND_r33] = _CALL_LIST_APPEND, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_O_r00] = _GUARD_CALLABLE_METHOD_DESCRIPTOR_O, + [_CALL_METHOD_DESCRIPTOR_O_r03] = _CALL_METHOD_DESCRIPTOR_O, + [_CHECK_RECURSION_LIMIT_r00] = _CHECK_RECURSION_LIMIT, + [_CHECK_RECURSION_LIMIT_r11] = _CHECK_RECURSION_LIMIT, + [_CHECK_RECURSION_LIMIT_r22] = _CHECK_RECURSION_LIMIT, + [_CHECK_RECURSION_LIMIT_r33] = _CHECK_RECURSION_LIMIT, + [_CALL_METHOD_DESCRIPTOR_O_INLINE_r03] = _CALL_METHOD_DESCRIPTOR_O_INLINE, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00] = _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS, + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00] = _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS, + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE_r00] = _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS_r00] = _GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS, + [_CALL_METHOD_DESCRIPTOR_NOARGS_r03] = _CALL_METHOD_DESCRIPTOR_NOARGS, + [_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE_r03] = _CALL_METHOD_DESCRIPTOR_NOARGS_INLINE, + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_r00] = _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST, + [_CALL_METHOD_DESCRIPTOR_FAST_r00] = _CALL_METHOD_DESCRIPTOR_FAST, + [_CALL_METHOD_DESCRIPTOR_FAST_INLINE_r00] = _CALL_METHOD_DESCRIPTOR_FAST_INLINE, + [_MAYBE_EXPAND_METHOD_KW_r11] = _MAYBE_EXPAND_METHOD_KW, + [_PY_FRAME_KW_r11] = _PY_FRAME_KW, + [_CHECK_FUNCTION_VERSION_KW_r11] = _CHECK_FUNCTION_VERSION_KW, + [_CHECK_METHOD_VERSION_KW_r11] = _CHECK_METHOD_VERSION_KW, + [_EXPAND_METHOD_KW_r11] = _EXPAND_METHOD_KW, + [_CHECK_IS_NOT_PY_CALLABLE_KW_r11] = _CHECK_IS_NOT_PY_CALLABLE_KW, + [_CALL_KW_NON_PY_r11] = _CALL_KW_NON_PY, + [_MAKE_CALLARGS_A_TUPLE_r33] = _MAKE_CALLARGS_A_TUPLE, + [_CHECK_IS_PY_CALLABLE_EX_r03] = _CHECK_IS_PY_CALLABLE_EX, + [_CHECK_IS_PY_CALLABLE_EX_r13] = _CHECK_IS_PY_CALLABLE_EX, + [_CHECK_IS_PY_CALLABLE_EX_r23] = _CHECK_IS_PY_CALLABLE_EX, + [_CHECK_IS_PY_CALLABLE_EX_r33] = _CHECK_IS_PY_CALLABLE_EX, + [_PY_FRAME_EX_r31] = _PY_FRAME_EX, + [_CHECK_IS_NOT_PY_CALLABLE_EX_r03] = _CHECK_IS_NOT_PY_CALLABLE_EX, + [_CHECK_IS_NOT_PY_CALLABLE_EX_r13] = _CHECK_IS_NOT_PY_CALLABLE_EX, + [_CHECK_IS_NOT_PY_CALLABLE_EX_r23] = _CHECK_IS_NOT_PY_CALLABLE_EX, + [_CHECK_IS_NOT_PY_CALLABLE_EX_r33] = _CHECK_IS_NOT_PY_CALLABLE_EX, + [_CALL_FUNCTION_EX_NON_PY_GENERAL_r31] = _CALL_FUNCTION_EX_NON_PY_GENERAL, + [_MAKE_FUNCTION_r12] = _MAKE_FUNCTION, + [_SET_FUNCTION_ATTRIBUTE_r01] = _SET_FUNCTION_ATTRIBUTE, + [_SET_FUNCTION_ATTRIBUTE_r11] = _SET_FUNCTION_ATTRIBUTE, + [_SET_FUNCTION_ATTRIBUTE_r21] = _SET_FUNCTION_ATTRIBUTE, + [_SET_FUNCTION_ATTRIBUTE_r32] = _SET_FUNCTION_ATTRIBUTE, + [_RETURN_GENERATOR_r01] = _RETURN_GENERATOR, + [_BUILD_SLICE_r01] = _BUILD_SLICE, + [_CONVERT_VALUE_r11] = _CONVERT_VALUE, + [_FORMAT_SIMPLE_r11] = _FORMAT_SIMPLE, + [_FORMAT_WITH_SPEC_r21] = _FORMAT_WITH_SPEC, + [_COPY_1_r02] = _COPY_1, + [_COPY_1_r12] = _COPY_1, + [_COPY_1_r23] = _COPY_1, + [_COPY_2_r03] = _COPY_2, + [_COPY_2_r13] = _COPY_2, + [_COPY_2_r23] = _COPY_2, + [_COPY_3_r03] = _COPY_3, + [_COPY_3_r13] = _COPY_3, + [_COPY_3_r23] = _COPY_3, + [_COPY_3_r33] = _COPY_3, + [_COPY_r01] = _COPY, + [_BINARY_OP_r23] = _BINARY_OP, + [_SWAP_2_r02] = _SWAP_2, + [_SWAP_2_r12] = _SWAP_2, + [_SWAP_2_r22] = _SWAP_2, + [_SWAP_2_r33] = _SWAP_2, + [_SWAP_3_r03] = _SWAP_3, + [_SWAP_3_r13] = _SWAP_3, + [_SWAP_3_r23] = _SWAP_3, + [_SWAP_3_r33] = _SWAP_3, + [_SWAP_r11] = _SWAP, + [_GUARD_IS_TRUE_POP_r00] = _GUARD_IS_TRUE_POP, + [_GUARD_IS_TRUE_POP_r10] = _GUARD_IS_TRUE_POP, + [_GUARD_IS_TRUE_POP_r21] = _GUARD_IS_TRUE_POP, + [_GUARD_IS_TRUE_POP_r32] = _GUARD_IS_TRUE_POP, + [_GUARD_IS_FALSE_POP_r00] = _GUARD_IS_FALSE_POP, + [_GUARD_IS_FALSE_POP_r10] = _GUARD_IS_FALSE_POP, + [_GUARD_IS_FALSE_POP_r21] = _GUARD_IS_FALSE_POP, + [_GUARD_IS_FALSE_POP_r32] = _GUARD_IS_FALSE_POP, + [_GUARD_BIT_IS_SET_POP_4_r00] = _GUARD_BIT_IS_SET_POP_4, + [_GUARD_BIT_IS_SET_POP_4_r10] = _GUARD_BIT_IS_SET_POP_4, + [_GUARD_BIT_IS_SET_POP_4_r21] = _GUARD_BIT_IS_SET_POP_4, + [_GUARD_BIT_IS_SET_POP_4_r32] = _GUARD_BIT_IS_SET_POP_4, + [_GUARD_BIT_IS_SET_POP_5_r00] = _GUARD_BIT_IS_SET_POP_5, + [_GUARD_BIT_IS_SET_POP_5_r10] = _GUARD_BIT_IS_SET_POP_5, + [_GUARD_BIT_IS_SET_POP_5_r21] = _GUARD_BIT_IS_SET_POP_5, + [_GUARD_BIT_IS_SET_POP_5_r32] = _GUARD_BIT_IS_SET_POP_5, + [_GUARD_BIT_IS_SET_POP_6_r00] = _GUARD_BIT_IS_SET_POP_6, + [_GUARD_BIT_IS_SET_POP_6_r10] = _GUARD_BIT_IS_SET_POP_6, + [_GUARD_BIT_IS_SET_POP_6_r21] = _GUARD_BIT_IS_SET_POP_6, + [_GUARD_BIT_IS_SET_POP_6_r32] = _GUARD_BIT_IS_SET_POP_6, + [_GUARD_BIT_IS_SET_POP_7_r00] = _GUARD_BIT_IS_SET_POP_7, + [_GUARD_BIT_IS_SET_POP_7_r10] = _GUARD_BIT_IS_SET_POP_7, + [_GUARD_BIT_IS_SET_POP_7_r21] = _GUARD_BIT_IS_SET_POP_7, + [_GUARD_BIT_IS_SET_POP_7_r32] = _GUARD_BIT_IS_SET_POP_7, + [_GUARD_BIT_IS_SET_POP_r00] = _GUARD_BIT_IS_SET_POP, + [_GUARD_BIT_IS_SET_POP_r10] = _GUARD_BIT_IS_SET_POP, + [_GUARD_BIT_IS_SET_POP_r21] = _GUARD_BIT_IS_SET_POP, + [_GUARD_BIT_IS_SET_POP_r32] = _GUARD_BIT_IS_SET_POP, + [_GUARD_BIT_IS_UNSET_POP_4_r00] = _GUARD_BIT_IS_UNSET_POP_4, + [_GUARD_BIT_IS_UNSET_POP_4_r10] = _GUARD_BIT_IS_UNSET_POP_4, + [_GUARD_BIT_IS_UNSET_POP_4_r21] = _GUARD_BIT_IS_UNSET_POP_4, + [_GUARD_BIT_IS_UNSET_POP_4_r32] = _GUARD_BIT_IS_UNSET_POP_4, + [_GUARD_BIT_IS_UNSET_POP_5_r00] = _GUARD_BIT_IS_UNSET_POP_5, + [_GUARD_BIT_IS_UNSET_POP_5_r10] = _GUARD_BIT_IS_UNSET_POP_5, + [_GUARD_BIT_IS_UNSET_POP_5_r21] = _GUARD_BIT_IS_UNSET_POP_5, + [_GUARD_BIT_IS_UNSET_POP_5_r32] = _GUARD_BIT_IS_UNSET_POP_5, + [_GUARD_BIT_IS_UNSET_POP_6_r00] = _GUARD_BIT_IS_UNSET_POP_6, + [_GUARD_BIT_IS_UNSET_POP_6_r10] = _GUARD_BIT_IS_UNSET_POP_6, + [_GUARD_BIT_IS_UNSET_POP_6_r21] = _GUARD_BIT_IS_UNSET_POP_6, + [_GUARD_BIT_IS_UNSET_POP_6_r32] = _GUARD_BIT_IS_UNSET_POP_6, + [_GUARD_BIT_IS_UNSET_POP_7_r00] = _GUARD_BIT_IS_UNSET_POP_7, + [_GUARD_BIT_IS_UNSET_POP_7_r10] = _GUARD_BIT_IS_UNSET_POP_7, + [_GUARD_BIT_IS_UNSET_POP_7_r21] = _GUARD_BIT_IS_UNSET_POP_7, + [_GUARD_BIT_IS_UNSET_POP_7_r32] = _GUARD_BIT_IS_UNSET_POP_7, + [_GUARD_BIT_IS_UNSET_POP_r00] = _GUARD_BIT_IS_UNSET_POP, + [_GUARD_BIT_IS_UNSET_POP_r10] = _GUARD_BIT_IS_UNSET_POP, + [_GUARD_BIT_IS_UNSET_POP_r21] = _GUARD_BIT_IS_UNSET_POP, + [_GUARD_BIT_IS_UNSET_POP_r32] = _GUARD_BIT_IS_UNSET_POP, + [_GUARD_IS_NONE_POP_r00] = _GUARD_IS_NONE_POP, + [_GUARD_IS_NONE_POP_r10] = _GUARD_IS_NONE_POP, + [_GUARD_IS_NONE_POP_r21] = _GUARD_IS_NONE_POP, + [_GUARD_IS_NONE_POP_r32] = _GUARD_IS_NONE_POP, + [_GUARD_IS_NOT_NONE_POP_r10] = _GUARD_IS_NOT_NONE_POP, + [_JUMP_TO_TOP_r00] = _JUMP_TO_TOP, + [_SET_IP_r00] = _SET_IP, + [_SET_IP_r11] = _SET_IP, + [_SET_IP_r22] = _SET_IP, + [_SET_IP_r33] = _SET_IP, + [_CHECK_STACK_SPACE_OPERAND_r00] = _CHECK_STACK_SPACE_OPERAND, + [_CHECK_STACK_SPACE_OPERAND_r11] = _CHECK_STACK_SPACE_OPERAND, + [_CHECK_STACK_SPACE_OPERAND_r22] = _CHECK_STACK_SPACE_OPERAND, + [_CHECK_STACK_SPACE_OPERAND_r33] = _CHECK_STACK_SPACE_OPERAND, + [_SAVE_RETURN_OFFSET_r00] = _SAVE_RETURN_OFFSET, + [_SAVE_RETURN_OFFSET_r11] = _SAVE_RETURN_OFFSET, + [_SAVE_RETURN_OFFSET_r22] = _SAVE_RETURN_OFFSET, + [_SAVE_RETURN_OFFSET_r33] = _SAVE_RETURN_OFFSET, + [_EXIT_TRACE_r00] = _EXIT_TRACE, + [_EXIT_TRACE_r10] = _EXIT_TRACE, + [_EXIT_TRACE_r20] = _EXIT_TRACE, + [_EXIT_TRACE_r30] = _EXIT_TRACE, + [_DYNAMIC_EXIT_r00] = _DYNAMIC_EXIT, + [_DYNAMIC_EXIT_r10] = _DYNAMIC_EXIT, + [_DYNAMIC_EXIT_r20] = _DYNAMIC_EXIT, + [_DYNAMIC_EXIT_r30] = _DYNAMIC_EXIT, + [_CHECK_VALIDITY_r00] = _CHECK_VALIDITY, + [_CHECK_VALIDITY_r11] = _CHECK_VALIDITY, + [_CHECK_VALIDITY_r22] = _CHECK_VALIDITY, + [_CHECK_VALIDITY_r33] = _CHECK_VALIDITY, + [_LOAD_CONST_INLINE_r01] = _LOAD_CONST_INLINE, + [_LOAD_CONST_INLINE_r12] = _LOAD_CONST_INLINE, + [_LOAD_CONST_INLINE_r23] = _LOAD_CONST_INLINE, + [_LOAD_CONST_INLINE_BORROW_r01] = _LOAD_CONST_INLINE_BORROW, + [_LOAD_CONST_INLINE_BORROW_r12] = _LOAD_CONST_INLINE_BORROW, + [_LOAD_CONST_INLINE_BORROW_r23] = _LOAD_CONST_INLINE_BORROW, + [_RROT_3_r03] = _RROT_3, + [_RROT_3_r13] = _RROT_3, + [_RROT_3_r23] = _RROT_3, + [_RROT_3_r33] = _RROT_3, + [_START_EXECUTOR_r00] = _START_EXECUTOR, + [_MAKE_WARM_r00] = _MAKE_WARM, + [_MAKE_WARM_r11] = _MAKE_WARM, + [_MAKE_WARM_r22] = _MAKE_WARM, + [_MAKE_WARM_r33] = _MAKE_WARM, + [_FATAL_ERROR_r00] = _FATAL_ERROR, + [_FATAL_ERROR_r11] = _FATAL_ERROR, + [_FATAL_ERROR_r22] = _FATAL_ERROR, + [_FATAL_ERROR_r33] = _FATAL_ERROR, + [_DEOPT_r00] = _DEOPT, + [_DEOPT_r10] = _DEOPT, + [_DEOPT_r20] = _DEOPT, + [_DEOPT_r30] = _DEOPT, + [_HANDLE_PENDING_AND_DEOPT_r00] = _HANDLE_PENDING_AND_DEOPT, + [_HANDLE_PENDING_AND_DEOPT_r10] = _HANDLE_PENDING_AND_DEOPT, + [_HANDLE_PENDING_AND_DEOPT_r20] = _HANDLE_PENDING_AND_DEOPT, + [_HANDLE_PENDING_AND_DEOPT_r30] = _HANDLE_PENDING_AND_DEOPT, + [_ERROR_POP_N_r00] = _ERROR_POP_N, + [_SPILL_OR_RELOAD_r01] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r02] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r03] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r10] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r12] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r13] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r20] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r21] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r23] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r30] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r31] = _SPILL_OR_RELOAD, + [_SPILL_OR_RELOAD_r32] = _SPILL_OR_RELOAD, + [_TIER2_RESUME_CHECK_r00] = _TIER2_RESUME_CHECK, + [_TIER2_RESUME_CHECK_r11] = _TIER2_RESUME_CHECK, + [_TIER2_RESUME_CHECK_r22] = _TIER2_RESUME_CHECK, + [_TIER2_RESUME_CHECK_r33] = _TIER2_RESUME_CHECK, + [_COLD_EXIT_r00] = _COLD_EXIT, + [_COLD_DYNAMIC_EXIT_r00] = _COLD_DYNAMIC_EXIT, + [_GUARD_CODE_VERSION__PUSH_FRAME_r00] = _GUARD_CODE_VERSION__PUSH_FRAME, + [_GUARD_CODE_VERSION__PUSH_FRAME_r11] = _GUARD_CODE_VERSION__PUSH_FRAME, + [_GUARD_CODE_VERSION__PUSH_FRAME_r22] = _GUARD_CODE_VERSION__PUSH_FRAME, + [_GUARD_CODE_VERSION__PUSH_FRAME_r33] = _GUARD_CODE_VERSION__PUSH_FRAME, + [_GUARD_CODE_VERSION_YIELD_VALUE_r00] = _GUARD_CODE_VERSION_YIELD_VALUE, + [_GUARD_CODE_VERSION_YIELD_VALUE_r11] = _GUARD_CODE_VERSION_YIELD_VALUE, + [_GUARD_CODE_VERSION_YIELD_VALUE_r22] = _GUARD_CODE_VERSION_YIELD_VALUE, + [_GUARD_CODE_VERSION_YIELD_VALUE_r33] = _GUARD_CODE_VERSION_YIELD_VALUE, + [_GUARD_CODE_VERSION_RETURN_VALUE_r00] = _GUARD_CODE_VERSION_RETURN_VALUE, + [_GUARD_CODE_VERSION_RETURN_VALUE_r11] = _GUARD_CODE_VERSION_RETURN_VALUE, + [_GUARD_CODE_VERSION_RETURN_VALUE_r22] = _GUARD_CODE_VERSION_RETURN_VALUE, + [_GUARD_CODE_VERSION_RETURN_VALUE_r33] = _GUARD_CODE_VERSION_RETURN_VALUE, + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r00] = _GUARD_CODE_VERSION_RETURN_GENERATOR, + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r11] = _GUARD_CODE_VERSION_RETURN_GENERATOR, + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r22] = _GUARD_CODE_VERSION_RETURN_GENERATOR, + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r33] = _GUARD_CODE_VERSION_RETURN_GENERATOR, + [_GUARD_IP__PUSH_FRAME_r00] = _GUARD_IP__PUSH_FRAME, + [_GUARD_IP__PUSH_FRAME_r11] = _GUARD_IP__PUSH_FRAME, + [_GUARD_IP__PUSH_FRAME_r22] = _GUARD_IP__PUSH_FRAME, + [_GUARD_IP__PUSH_FRAME_r33] = _GUARD_IP__PUSH_FRAME, + [_GUARD_IP_YIELD_VALUE_r00] = _GUARD_IP_YIELD_VALUE, + [_GUARD_IP_YIELD_VALUE_r11] = _GUARD_IP_YIELD_VALUE, + [_GUARD_IP_YIELD_VALUE_r22] = _GUARD_IP_YIELD_VALUE, + [_GUARD_IP_YIELD_VALUE_r33] = _GUARD_IP_YIELD_VALUE, + [_GUARD_IP_RETURN_VALUE_r00] = _GUARD_IP_RETURN_VALUE, + [_GUARD_IP_RETURN_VALUE_r11] = _GUARD_IP_RETURN_VALUE, + [_GUARD_IP_RETURN_VALUE_r22] = _GUARD_IP_RETURN_VALUE, + [_GUARD_IP_RETURN_VALUE_r33] = _GUARD_IP_RETURN_VALUE, + [_GUARD_IP_RETURN_GENERATOR_r00] = _GUARD_IP_RETURN_GENERATOR, + [_GUARD_IP_RETURN_GENERATOR_r11] = _GUARD_IP_RETURN_GENERATOR, + [_GUARD_IP_RETURN_GENERATOR_r22] = _GUARD_IP_RETURN_GENERATOR, + [_GUARD_IP_RETURN_GENERATOR_r33] = _GUARD_IP_RETURN_GENERATOR, }; -const char *const _PyOpcode_uop_name[MAX_UOP_ID+1] = { +const uint16_t _PyUop_SpillsAndReloads[4][4] = { + [0][1] = _SPILL_OR_RELOAD_r01, + [0][2] = _SPILL_OR_RELOAD_r02, + [0][3] = _SPILL_OR_RELOAD_r03, + [1][0] = _SPILL_OR_RELOAD_r10, + [1][2] = _SPILL_OR_RELOAD_r12, + [1][3] = _SPILL_OR_RELOAD_r13, + [2][0] = _SPILL_OR_RELOAD_r20, + [2][1] = _SPILL_OR_RELOAD_r21, + [2][3] = _SPILL_OR_RELOAD_r23, + [3][0] = _SPILL_OR_RELOAD_r30, + [3][1] = _SPILL_OR_RELOAD_r31, + [3][2] = _SPILL_OR_RELOAD_r32, +}; + +const char *const _PyOpcode_uop_name[MAX_UOP_REGS_ID+1] = { + [_ALLOCATE_OBJECT] = "_ALLOCATE_OBJECT", + [_ALLOCATE_OBJECT_r00] = "_ALLOCATE_OBJECT_r00", [_BINARY_OP] = "_BINARY_OP", + [_BINARY_OP_r23] = "_BINARY_OP_r23", [_BINARY_OP_ADD_FLOAT] = "_BINARY_OP_ADD_FLOAT", + [_BINARY_OP_ADD_FLOAT_r03] = "_BINARY_OP_ADD_FLOAT_r03", + [_BINARY_OP_ADD_FLOAT_r13] = "_BINARY_OP_ADD_FLOAT_r13", + [_BINARY_OP_ADD_FLOAT_r23] = "_BINARY_OP_ADD_FLOAT_r23", + [_BINARY_OP_ADD_FLOAT_INPLACE] = "_BINARY_OP_ADD_FLOAT_INPLACE", + [_BINARY_OP_ADD_FLOAT_INPLACE_r03] = "_BINARY_OP_ADD_FLOAT_INPLACE_r03", + [_BINARY_OP_ADD_FLOAT_INPLACE_r13] = "_BINARY_OP_ADD_FLOAT_INPLACE_r13", + [_BINARY_OP_ADD_FLOAT_INPLACE_r23] = "_BINARY_OP_ADD_FLOAT_INPLACE_r23", + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT] = "_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT", + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r03] = "_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r03", + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r13] = "_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r13", + [_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r23] = "_BINARY_OP_ADD_FLOAT_INPLACE_RIGHT_r23", [_BINARY_OP_ADD_INT] = "_BINARY_OP_ADD_INT", + [_BINARY_OP_ADD_INT_r03] = "_BINARY_OP_ADD_INT_r03", + [_BINARY_OP_ADD_INT_r13] = "_BINARY_OP_ADD_INT_r13", + [_BINARY_OP_ADD_INT_r23] = "_BINARY_OP_ADD_INT_r23", + [_BINARY_OP_ADD_INT_INPLACE] = "_BINARY_OP_ADD_INT_INPLACE", + [_BINARY_OP_ADD_INT_INPLACE_r03] = "_BINARY_OP_ADD_INT_INPLACE_r03", + [_BINARY_OP_ADD_INT_INPLACE_r13] = "_BINARY_OP_ADD_INT_INPLACE_r13", + [_BINARY_OP_ADD_INT_INPLACE_r23] = "_BINARY_OP_ADD_INT_INPLACE_r23", + [_BINARY_OP_ADD_INT_INPLACE_RIGHT] = "_BINARY_OP_ADD_INT_INPLACE_RIGHT", + [_BINARY_OP_ADD_INT_INPLACE_RIGHT_r03] = "_BINARY_OP_ADD_INT_INPLACE_RIGHT_r03", + [_BINARY_OP_ADD_INT_INPLACE_RIGHT_r13] = "_BINARY_OP_ADD_INT_INPLACE_RIGHT_r13", + [_BINARY_OP_ADD_INT_INPLACE_RIGHT_r23] = "_BINARY_OP_ADD_INT_INPLACE_RIGHT_r23", [_BINARY_OP_ADD_UNICODE] = "_BINARY_OP_ADD_UNICODE", + [_BINARY_OP_ADD_UNICODE_r03] = "_BINARY_OP_ADD_UNICODE_r03", + [_BINARY_OP_ADD_UNICODE_r13] = "_BINARY_OP_ADD_UNICODE_r13", + [_BINARY_OP_ADD_UNICODE_r23] = "_BINARY_OP_ADD_UNICODE_r23", [_BINARY_OP_EXTEND] = "_BINARY_OP_EXTEND", + [_BINARY_OP_EXTEND_r23] = "_BINARY_OP_EXTEND_r23", [_BINARY_OP_INPLACE_ADD_UNICODE] = "_BINARY_OP_INPLACE_ADD_UNICODE", + [_BINARY_OP_INPLACE_ADD_UNICODE_r21] = "_BINARY_OP_INPLACE_ADD_UNICODE_r21", [_BINARY_OP_MULTIPLY_FLOAT] = "_BINARY_OP_MULTIPLY_FLOAT", + [_BINARY_OP_MULTIPLY_FLOAT_r03] = "_BINARY_OP_MULTIPLY_FLOAT_r03", + [_BINARY_OP_MULTIPLY_FLOAT_r13] = "_BINARY_OP_MULTIPLY_FLOAT_r13", + [_BINARY_OP_MULTIPLY_FLOAT_r23] = "_BINARY_OP_MULTIPLY_FLOAT_r23", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r03] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r03", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r13] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r13", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r23] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE_r23", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r03] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r03", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r13] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r13", + [_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r23] = "_BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT_r23", [_BINARY_OP_MULTIPLY_INT] = "_BINARY_OP_MULTIPLY_INT", + [_BINARY_OP_MULTIPLY_INT_r03] = "_BINARY_OP_MULTIPLY_INT_r03", + [_BINARY_OP_MULTIPLY_INT_r13] = "_BINARY_OP_MULTIPLY_INT_r13", + [_BINARY_OP_MULTIPLY_INT_r23] = "_BINARY_OP_MULTIPLY_INT_r23", + [_BINARY_OP_MULTIPLY_INT_INPLACE] = "_BINARY_OP_MULTIPLY_INT_INPLACE", + [_BINARY_OP_MULTIPLY_INT_INPLACE_r03] = "_BINARY_OP_MULTIPLY_INT_INPLACE_r03", + [_BINARY_OP_MULTIPLY_INT_INPLACE_r13] = "_BINARY_OP_MULTIPLY_INT_INPLACE_r13", + [_BINARY_OP_MULTIPLY_INT_INPLACE_r23] = "_BINARY_OP_MULTIPLY_INT_INPLACE_r23", + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT] = "_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT", + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r03] = "_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r03", + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r13] = "_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r13", + [_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r23] = "_BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT_r23", [_BINARY_OP_SUBSCR_CHECK_FUNC] = "_BINARY_OP_SUBSCR_CHECK_FUNC", + [_BINARY_OP_SUBSCR_CHECK_FUNC_r23] = "_BINARY_OP_SUBSCR_CHECK_FUNC_r23", [_BINARY_OP_SUBSCR_DICT] = "_BINARY_OP_SUBSCR_DICT", + [_BINARY_OP_SUBSCR_DICT_r23] = "_BINARY_OP_SUBSCR_DICT_r23", + [_BINARY_OP_SUBSCR_DICT_KNOWN_HASH] = "_BINARY_OP_SUBSCR_DICT_KNOWN_HASH", + [_BINARY_OP_SUBSCR_DICT_KNOWN_HASH_r23] = "_BINARY_OP_SUBSCR_DICT_KNOWN_HASH_r23", [_BINARY_OP_SUBSCR_INIT_CALL] = "_BINARY_OP_SUBSCR_INIT_CALL", + [_BINARY_OP_SUBSCR_INIT_CALL_r01] = "_BINARY_OP_SUBSCR_INIT_CALL_r01", + [_BINARY_OP_SUBSCR_INIT_CALL_r11] = "_BINARY_OP_SUBSCR_INIT_CALL_r11", + [_BINARY_OP_SUBSCR_INIT_CALL_r21] = "_BINARY_OP_SUBSCR_INIT_CALL_r21", + [_BINARY_OP_SUBSCR_INIT_CALL_r31] = "_BINARY_OP_SUBSCR_INIT_CALL_r31", [_BINARY_OP_SUBSCR_LIST_INT] = "_BINARY_OP_SUBSCR_LIST_INT", + [_BINARY_OP_SUBSCR_LIST_INT_r23] = "_BINARY_OP_SUBSCR_LIST_INT_r23", [_BINARY_OP_SUBSCR_LIST_SLICE] = "_BINARY_OP_SUBSCR_LIST_SLICE", + [_BINARY_OP_SUBSCR_LIST_SLICE_r23] = "_BINARY_OP_SUBSCR_LIST_SLICE_r23", [_BINARY_OP_SUBSCR_STR_INT] = "_BINARY_OP_SUBSCR_STR_INT", + [_BINARY_OP_SUBSCR_STR_INT_r23] = "_BINARY_OP_SUBSCR_STR_INT_r23", [_BINARY_OP_SUBSCR_TUPLE_INT] = "_BINARY_OP_SUBSCR_TUPLE_INT", + [_BINARY_OP_SUBSCR_TUPLE_INT_r03] = "_BINARY_OP_SUBSCR_TUPLE_INT_r03", + [_BINARY_OP_SUBSCR_TUPLE_INT_r13] = "_BINARY_OP_SUBSCR_TUPLE_INT_r13", + [_BINARY_OP_SUBSCR_TUPLE_INT_r23] = "_BINARY_OP_SUBSCR_TUPLE_INT_r23", + [_BINARY_OP_SUBSCR_USTR_INT] = "_BINARY_OP_SUBSCR_USTR_INT", + [_BINARY_OP_SUBSCR_USTR_INT_r23] = "_BINARY_OP_SUBSCR_USTR_INT_r23", [_BINARY_OP_SUBTRACT_FLOAT] = "_BINARY_OP_SUBTRACT_FLOAT", + [_BINARY_OP_SUBTRACT_FLOAT_r03] = "_BINARY_OP_SUBTRACT_FLOAT_r03", + [_BINARY_OP_SUBTRACT_FLOAT_r13] = "_BINARY_OP_SUBTRACT_FLOAT_r13", + [_BINARY_OP_SUBTRACT_FLOAT_r23] = "_BINARY_OP_SUBTRACT_FLOAT_r23", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r03] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r03", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r13] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r13", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r23] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE_r23", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r03] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r03", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r13] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r13", + [_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r23] = "_BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT_r23", [_BINARY_OP_SUBTRACT_INT] = "_BINARY_OP_SUBTRACT_INT", + [_BINARY_OP_SUBTRACT_INT_r03] = "_BINARY_OP_SUBTRACT_INT_r03", + [_BINARY_OP_SUBTRACT_INT_r13] = "_BINARY_OP_SUBTRACT_INT_r13", + [_BINARY_OP_SUBTRACT_INT_r23] = "_BINARY_OP_SUBTRACT_INT_r23", + [_BINARY_OP_SUBTRACT_INT_INPLACE] = "_BINARY_OP_SUBTRACT_INT_INPLACE", + [_BINARY_OP_SUBTRACT_INT_INPLACE_r03] = "_BINARY_OP_SUBTRACT_INT_INPLACE_r03", + [_BINARY_OP_SUBTRACT_INT_INPLACE_r13] = "_BINARY_OP_SUBTRACT_INT_INPLACE_r13", + [_BINARY_OP_SUBTRACT_INT_INPLACE_r23] = "_BINARY_OP_SUBTRACT_INT_INPLACE_r23", + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT] = "_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT", + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r03] = "_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r03", + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r13] = "_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r13", + [_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r23] = "_BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT_r23", + [_BINARY_OP_TRUEDIV_FLOAT] = "_BINARY_OP_TRUEDIV_FLOAT", + [_BINARY_OP_TRUEDIV_FLOAT_r23] = "_BINARY_OP_TRUEDIV_FLOAT_r23", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r03] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r03", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r13] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r13", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r23] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE_r23", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r03] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r03", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r13] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r13", + [_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r23] = "_BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT_r23", [_BINARY_SLICE] = "_BINARY_SLICE", + [_BINARY_SLICE_r31] = "_BINARY_SLICE_r31", [_BUILD_INTERPOLATION] = "_BUILD_INTERPOLATION", + [_BUILD_INTERPOLATION_r01] = "_BUILD_INTERPOLATION_r01", [_BUILD_LIST] = "_BUILD_LIST", + [_BUILD_LIST_r01] = "_BUILD_LIST_r01", [_BUILD_MAP] = "_BUILD_MAP", + [_BUILD_MAP_r01] = "_BUILD_MAP_r01", [_BUILD_SET] = "_BUILD_SET", + [_BUILD_SET_r01] = "_BUILD_SET_r01", [_BUILD_SLICE] = "_BUILD_SLICE", + [_BUILD_SLICE_r01] = "_BUILD_SLICE_r01", [_BUILD_STRING] = "_BUILD_STRING", + [_BUILD_STRING_r01] = "_BUILD_STRING_r01", [_BUILD_TEMPLATE] = "_BUILD_TEMPLATE", + [_BUILD_TEMPLATE_r21] = "_BUILD_TEMPLATE_r21", [_BUILD_TUPLE] = "_BUILD_TUPLE", + [_BUILD_TUPLE_r01] = "_BUILD_TUPLE_r01", [_CALL_BUILTIN_CLASS] = "_CALL_BUILTIN_CLASS", + [_CALL_BUILTIN_CLASS_r00] = "_CALL_BUILTIN_CLASS_r00", [_CALL_BUILTIN_FAST] = "_CALL_BUILTIN_FAST", + [_CALL_BUILTIN_FAST_r00] = "_CALL_BUILTIN_FAST_r00", [_CALL_BUILTIN_FAST_WITH_KEYWORDS] = "_CALL_BUILTIN_FAST_WITH_KEYWORDS", + [_CALL_BUILTIN_FAST_WITH_KEYWORDS_r00] = "_CALL_BUILTIN_FAST_WITH_KEYWORDS_r00", [_CALL_BUILTIN_O] = "_CALL_BUILTIN_O", + [_CALL_BUILTIN_O_r03] = "_CALL_BUILTIN_O_r03", + [_CALL_FUNCTION_EX_NON_PY_GENERAL] = "_CALL_FUNCTION_EX_NON_PY_GENERAL", + [_CALL_FUNCTION_EX_NON_PY_GENERAL_r31] = "_CALL_FUNCTION_EX_NON_PY_GENERAL_r31", [_CALL_INTRINSIC_1] = "_CALL_INTRINSIC_1", + [_CALL_INTRINSIC_1_r12] = "_CALL_INTRINSIC_1_r12", [_CALL_INTRINSIC_2] = "_CALL_INTRINSIC_2", + [_CALL_INTRINSIC_2_r23] = "_CALL_INTRINSIC_2_r23", [_CALL_ISINSTANCE] = "_CALL_ISINSTANCE", + [_CALL_ISINSTANCE_r31] = "_CALL_ISINSTANCE_r31", [_CALL_KW_NON_PY] = "_CALL_KW_NON_PY", + [_CALL_KW_NON_PY_r11] = "_CALL_KW_NON_PY_r11", [_CALL_LEN] = "_CALL_LEN", + [_CALL_LEN_r33] = "_CALL_LEN_r33", [_CALL_LIST_APPEND] = "_CALL_LIST_APPEND", + [_CALL_LIST_APPEND_r03] = "_CALL_LIST_APPEND_r03", + [_CALL_LIST_APPEND_r13] = "_CALL_LIST_APPEND_r13", + [_CALL_LIST_APPEND_r23] = "_CALL_LIST_APPEND_r23", + [_CALL_LIST_APPEND_r33] = "_CALL_LIST_APPEND_r33", [_CALL_METHOD_DESCRIPTOR_FAST] = "_CALL_METHOD_DESCRIPTOR_FAST", + [_CALL_METHOD_DESCRIPTOR_FAST_r00] = "_CALL_METHOD_DESCRIPTOR_FAST_r00", + [_CALL_METHOD_DESCRIPTOR_FAST_INLINE] = "_CALL_METHOD_DESCRIPTOR_FAST_INLINE", + [_CALL_METHOD_DESCRIPTOR_FAST_INLINE_r00] = "_CALL_METHOD_DESCRIPTOR_FAST_INLINE_r00", [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = "_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS", + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00] = "_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00", + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE] = "_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE", + [_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE_r00] = "_CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE_r00", [_CALL_METHOD_DESCRIPTOR_NOARGS] = "_CALL_METHOD_DESCRIPTOR_NOARGS", + [_CALL_METHOD_DESCRIPTOR_NOARGS_r03] = "_CALL_METHOD_DESCRIPTOR_NOARGS_r03", + [_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE] = "_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE", + [_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE_r03] = "_CALL_METHOD_DESCRIPTOR_NOARGS_INLINE_r03", [_CALL_METHOD_DESCRIPTOR_O] = "_CALL_METHOD_DESCRIPTOR_O", + [_CALL_METHOD_DESCRIPTOR_O_r03] = "_CALL_METHOD_DESCRIPTOR_O_r03", + [_CALL_METHOD_DESCRIPTOR_O_INLINE] = "_CALL_METHOD_DESCRIPTOR_O_INLINE", + [_CALL_METHOD_DESCRIPTOR_O_INLINE_r03] = "_CALL_METHOD_DESCRIPTOR_O_INLINE_r03", [_CALL_NON_PY_GENERAL] = "_CALL_NON_PY_GENERAL", + [_CALL_NON_PY_GENERAL_r01] = "_CALL_NON_PY_GENERAL_r01", [_CALL_STR_1] = "_CALL_STR_1", + [_CALL_STR_1_r32] = "_CALL_STR_1_r32", [_CALL_TUPLE_1] = "_CALL_TUPLE_1", + [_CALL_TUPLE_1_r32] = "_CALL_TUPLE_1_r32", [_CALL_TYPE_1] = "_CALL_TYPE_1", - [_CHECK_AND_ALLOCATE_OBJECT] = "_CHECK_AND_ALLOCATE_OBJECT", + [_CALL_TYPE_1_r02] = "_CALL_TYPE_1_r02", + [_CALL_TYPE_1_r12] = "_CALL_TYPE_1_r12", + [_CALL_TYPE_1_r22] = "_CALL_TYPE_1_r22", + [_CALL_TYPE_1_r32] = "_CALL_TYPE_1_r32", [_CHECK_ATTR_CLASS] = "_CHECK_ATTR_CLASS", + [_CHECK_ATTR_CLASS_r01] = "_CHECK_ATTR_CLASS_r01", + [_CHECK_ATTR_CLASS_r11] = "_CHECK_ATTR_CLASS_r11", + [_CHECK_ATTR_CLASS_r22] = "_CHECK_ATTR_CLASS_r22", + [_CHECK_ATTR_CLASS_r33] = "_CHECK_ATTR_CLASS_r33", [_CHECK_ATTR_METHOD_LAZY_DICT] = "_CHECK_ATTR_METHOD_LAZY_DICT", + [_CHECK_ATTR_METHOD_LAZY_DICT_r01] = "_CHECK_ATTR_METHOD_LAZY_DICT_r01", + [_CHECK_ATTR_METHOD_LAZY_DICT_r11] = "_CHECK_ATTR_METHOD_LAZY_DICT_r11", + [_CHECK_ATTR_METHOD_LAZY_DICT_r22] = "_CHECK_ATTR_METHOD_LAZY_DICT_r22", + [_CHECK_ATTR_METHOD_LAZY_DICT_r33] = "_CHECK_ATTR_METHOD_LAZY_DICT_r33", [_CHECK_CALL_BOUND_METHOD_EXACT_ARGS] = "_CHECK_CALL_BOUND_METHOD_EXACT_ARGS", + [_CHECK_CALL_BOUND_METHOD_EXACT_ARGS_r00] = "_CHECK_CALL_BOUND_METHOD_EXACT_ARGS_r00", [_CHECK_EG_MATCH] = "_CHECK_EG_MATCH", + [_CHECK_EG_MATCH_r22] = "_CHECK_EG_MATCH_r22", [_CHECK_EXC_MATCH] = "_CHECK_EXC_MATCH", - [_CHECK_FUNCTION] = "_CHECK_FUNCTION", + [_CHECK_EXC_MATCH_r22] = "_CHECK_EXC_MATCH_r22", [_CHECK_FUNCTION_EXACT_ARGS] = "_CHECK_FUNCTION_EXACT_ARGS", + [_CHECK_FUNCTION_EXACT_ARGS_r00] = "_CHECK_FUNCTION_EXACT_ARGS_r00", [_CHECK_FUNCTION_VERSION] = "_CHECK_FUNCTION_VERSION", + [_CHECK_FUNCTION_VERSION_r00] = "_CHECK_FUNCTION_VERSION_r00", [_CHECK_FUNCTION_VERSION_INLINE] = "_CHECK_FUNCTION_VERSION_INLINE", + [_CHECK_FUNCTION_VERSION_INLINE_r00] = "_CHECK_FUNCTION_VERSION_INLINE_r00", + [_CHECK_FUNCTION_VERSION_INLINE_r11] = "_CHECK_FUNCTION_VERSION_INLINE_r11", + [_CHECK_FUNCTION_VERSION_INLINE_r22] = "_CHECK_FUNCTION_VERSION_INLINE_r22", + [_CHECK_FUNCTION_VERSION_INLINE_r33] = "_CHECK_FUNCTION_VERSION_INLINE_r33", [_CHECK_FUNCTION_VERSION_KW] = "_CHECK_FUNCTION_VERSION_KW", + [_CHECK_FUNCTION_VERSION_KW_r11] = "_CHECK_FUNCTION_VERSION_KW_r11", [_CHECK_IS_NOT_PY_CALLABLE] = "_CHECK_IS_NOT_PY_CALLABLE", + [_CHECK_IS_NOT_PY_CALLABLE_r00] = "_CHECK_IS_NOT_PY_CALLABLE_r00", + [_CHECK_IS_NOT_PY_CALLABLE_EX] = "_CHECK_IS_NOT_PY_CALLABLE_EX", + [_CHECK_IS_NOT_PY_CALLABLE_EX_r03] = "_CHECK_IS_NOT_PY_CALLABLE_EX_r03", + [_CHECK_IS_NOT_PY_CALLABLE_EX_r13] = "_CHECK_IS_NOT_PY_CALLABLE_EX_r13", + [_CHECK_IS_NOT_PY_CALLABLE_EX_r23] = "_CHECK_IS_NOT_PY_CALLABLE_EX_r23", + [_CHECK_IS_NOT_PY_CALLABLE_EX_r33] = "_CHECK_IS_NOT_PY_CALLABLE_EX_r33", [_CHECK_IS_NOT_PY_CALLABLE_KW] = "_CHECK_IS_NOT_PY_CALLABLE_KW", + [_CHECK_IS_NOT_PY_CALLABLE_KW_r11] = "_CHECK_IS_NOT_PY_CALLABLE_KW_r11", + [_CHECK_IS_PY_CALLABLE_EX] = "_CHECK_IS_PY_CALLABLE_EX", + [_CHECK_IS_PY_CALLABLE_EX_r03] = "_CHECK_IS_PY_CALLABLE_EX_r03", + [_CHECK_IS_PY_CALLABLE_EX_r13] = "_CHECK_IS_PY_CALLABLE_EX_r13", + [_CHECK_IS_PY_CALLABLE_EX_r23] = "_CHECK_IS_PY_CALLABLE_EX_r23", + [_CHECK_IS_PY_CALLABLE_EX_r33] = "_CHECK_IS_PY_CALLABLE_EX_r33", [_CHECK_MANAGED_OBJECT_HAS_VALUES] = "_CHECK_MANAGED_OBJECT_HAS_VALUES", + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r01] = "_CHECK_MANAGED_OBJECT_HAS_VALUES_r01", + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r11] = "_CHECK_MANAGED_OBJECT_HAS_VALUES_r11", + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r22] = "_CHECK_MANAGED_OBJECT_HAS_VALUES_r22", + [_CHECK_MANAGED_OBJECT_HAS_VALUES_r33] = "_CHECK_MANAGED_OBJECT_HAS_VALUES_r33", [_CHECK_METHOD_VERSION] = "_CHECK_METHOD_VERSION", + [_CHECK_METHOD_VERSION_r00] = "_CHECK_METHOD_VERSION_r00", [_CHECK_METHOD_VERSION_KW] = "_CHECK_METHOD_VERSION_KW", + [_CHECK_METHOD_VERSION_KW_r11] = "_CHECK_METHOD_VERSION_KW_r11", + [_CHECK_OBJECT] = "_CHECK_OBJECT", + [_CHECK_OBJECT_r00] = "_CHECK_OBJECT_r00", [_CHECK_PEP_523] = "_CHECK_PEP_523", + [_CHECK_PEP_523_r00] = "_CHECK_PEP_523_r00", + [_CHECK_PEP_523_r11] = "_CHECK_PEP_523_r11", + [_CHECK_PEP_523_r22] = "_CHECK_PEP_523_r22", + [_CHECK_PEP_523_r33] = "_CHECK_PEP_523_r33", [_CHECK_PERIODIC] = "_CHECK_PERIODIC", + [_CHECK_PERIODIC_r00] = "_CHECK_PERIODIC_r00", [_CHECK_PERIODIC_IF_NOT_YIELD_FROM] = "_CHECK_PERIODIC_IF_NOT_YIELD_FROM", + [_CHECK_PERIODIC_IF_NOT_YIELD_FROM_r00] = "_CHECK_PERIODIC_IF_NOT_YIELD_FROM_r00", + [_CHECK_RECURSION_LIMIT] = "_CHECK_RECURSION_LIMIT", + [_CHECK_RECURSION_LIMIT_r00] = "_CHECK_RECURSION_LIMIT_r00", + [_CHECK_RECURSION_LIMIT_r11] = "_CHECK_RECURSION_LIMIT_r11", + [_CHECK_RECURSION_LIMIT_r22] = "_CHECK_RECURSION_LIMIT_r22", + [_CHECK_RECURSION_LIMIT_r33] = "_CHECK_RECURSION_LIMIT_r33", [_CHECK_RECURSION_REMAINING] = "_CHECK_RECURSION_REMAINING", + [_CHECK_RECURSION_REMAINING_r00] = "_CHECK_RECURSION_REMAINING_r00", + [_CHECK_RECURSION_REMAINING_r11] = "_CHECK_RECURSION_REMAINING_r11", + [_CHECK_RECURSION_REMAINING_r22] = "_CHECK_RECURSION_REMAINING_r22", + [_CHECK_RECURSION_REMAINING_r33] = "_CHECK_RECURSION_REMAINING_r33", [_CHECK_STACK_SPACE] = "_CHECK_STACK_SPACE", + [_CHECK_STACK_SPACE_r00] = "_CHECK_STACK_SPACE_r00", [_CHECK_STACK_SPACE_OPERAND] = "_CHECK_STACK_SPACE_OPERAND", + [_CHECK_STACK_SPACE_OPERAND_r00] = "_CHECK_STACK_SPACE_OPERAND_r00", + [_CHECK_STACK_SPACE_OPERAND_r11] = "_CHECK_STACK_SPACE_OPERAND_r11", + [_CHECK_STACK_SPACE_OPERAND_r22] = "_CHECK_STACK_SPACE_OPERAND_r22", + [_CHECK_STACK_SPACE_OPERAND_r33] = "_CHECK_STACK_SPACE_OPERAND_r33", [_CHECK_VALIDITY] = "_CHECK_VALIDITY", + [_CHECK_VALIDITY_r00] = "_CHECK_VALIDITY_r00", + [_CHECK_VALIDITY_r11] = "_CHECK_VALIDITY_r11", + [_CHECK_VALIDITY_r22] = "_CHECK_VALIDITY_r22", + [_CHECK_VALIDITY_r33] = "_CHECK_VALIDITY_r33", + [_COLD_DYNAMIC_EXIT] = "_COLD_DYNAMIC_EXIT", + [_COLD_DYNAMIC_EXIT_r00] = "_COLD_DYNAMIC_EXIT_r00", + [_COLD_EXIT] = "_COLD_EXIT", + [_COLD_EXIT_r00] = "_COLD_EXIT_r00", [_COMPARE_OP] = "_COMPARE_OP", + [_COMPARE_OP_r21] = "_COMPARE_OP_r21", [_COMPARE_OP_FLOAT] = "_COMPARE_OP_FLOAT", + [_COMPARE_OP_FLOAT_r03] = "_COMPARE_OP_FLOAT_r03", + [_COMPARE_OP_FLOAT_r13] = "_COMPARE_OP_FLOAT_r13", + [_COMPARE_OP_FLOAT_r23] = "_COMPARE_OP_FLOAT_r23", [_COMPARE_OP_INT] = "_COMPARE_OP_INT", + [_COMPARE_OP_INT_r23] = "_COMPARE_OP_INT_r23", [_COMPARE_OP_STR] = "_COMPARE_OP_STR", + [_COMPARE_OP_STR_r23] = "_COMPARE_OP_STR_r23", [_CONTAINS_OP] = "_CONTAINS_OP", + [_CONTAINS_OP_r23] = "_CONTAINS_OP_r23", [_CONTAINS_OP_DICT] = "_CONTAINS_OP_DICT", + [_CONTAINS_OP_DICT_r23] = "_CONTAINS_OP_DICT_r23", [_CONTAINS_OP_SET] = "_CONTAINS_OP_SET", + [_CONTAINS_OP_SET_r23] = "_CONTAINS_OP_SET_r23", [_CONVERT_VALUE] = "_CONVERT_VALUE", + [_CONVERT_VALUE_r11] = "_CONVERT_VALUE_r11", [_COPY] = "_COPY", + [_COPY_r01] = "_COPY_r01", + [_COPY_1] = "_COPY_1", + [_COPY_1_r02] = "_COPY_1_r02", + [_COPY_1_r12] = "_COPY_1_r12", + [_COPY_1_r23] = "_COPY_1_r23", + [_COPY_2] = "_COPY_2", + [_COPY_2_r03] = "_COPY_2_r03", + [_COPY_2_r13] = "_COPY_2_r13", + [_COPY_2_r23] = "_COPY_2_r23", + [_COPY_3] = "_COPY_3", + [_COPY_3_r03] = "_COPY_3_r03", + [_COPY_3_r13] = "_COPY_3_r13", + [_COPY_3_r23] = "_COPY_3_r23", + [_COPY_3_r33] = "_COPY_3_r33", [_COPY_FREE_VARS] = "_COPY_FREE_VARS", + [_COPY_FREE_VARS_r00] = "_COPY_FREE_VARS_r00", + [_COPY_FREE_VARS_r11] = "_COPY_FREE_VARS_r11", + [_COPY_FREE_VARS_r22] = "_COPY_FREE_VARS_r22", + [_COPY_FREE_VARS_r33] = "_COPY_FREE_VARS_r33", [_CREATE_INIT_FRAME] = "_CREATE_INIT_FRAME", + [_CREATE_INIT_FRAME_r01] = "_CREATE_INIT_FRAME_r01", [_DELETE_ATTR] = "_DELETE_ATTR", + [_DELETE_ATTR_r10] = "_DELETE_ATTR_r10", [_DELETE_DEREF] = "_DELETE_DEREF", + [_DELETE_DEREF_r00] = "_DELETE_DEREF_r00", [_DELETE_FAST] = "_DELETE_FAST", + [_DELETE_FAST_r00] = "_DELETE_FAST_r00", [_DELETE_GLOBAL] = "_DELETE_GLOBAL", + [_DELETE_GLOBAL_r00] = "_DELETE_GLOBAL_r00", [_DELETE_NAME] = "_DELETE_NAME", + [_DELETE_NAME_r00] = "_DELETE_NAME_r00", [_DELETE_SUBSCR] = "_DELETE_SUBSCR", + [_DELETE_SUBSCR_r20] = "_DELETE_SUBSCR_r20", [_DEOPT] = "_DEOPT", + [_DEOPT_r00] = "_DEOPT_r00", + [_DEOPT_r10] = "_DEOPT_r10", + [_DEOPT_r20] = "_DEOPT_r20", + [_DEOPT_r30] = "_DEOPT_r30", [_DICT_MERGE] = "_DICT_MERGE", + [_DICT_MERGE_r11] = "_DICT_MERGE_r11", [_DICT_UPDATE] = "_DICT_UPDATE", + [_DICT_UPDATE_r11] = "_DICT_UPDATE_r11", + [_DYNAMIC_EXIT] = "_DYNAMIC_EXIT", + [_DYNAMIC_EXIT_r00] = "_DYNAMIC_EXIT_r00", + [_DYNAMIC_EXIT_r10] = "_DYNAMIC_EXIT_r10", + [_DYNAMIC_EXIT_r20] = "_DYNAMIC_EXIT_r20", + [_DYNAMIC_EXIT_r30] = "_DYNAMIC_EXIT_r30", [_END_FOR] = "_END_FOR", + [_END_FOR_r10] = "_END_FOR_r10", [_END_SEND] = "_END_SEND", + [_END_SEND_r31] = "_END_SEND_r31", [_ERROR_POP_N] = "_ERROR_POP_N", + [_ERROR_POP_N_r00] = "_ERROR_POP_N_r00", [_EXIT_INIT_CHECK] = "_EXIT_INIT_CHECK", + [_EXIT_INIT_CHECK_r10] = "_EXIT_INIT_CHECK_r10", [_EXIT_TRACE] = "_EXIT_TRACE", + [_EXIT_TRACE_r00] = "_EXIT_TRACE_r00", + [_EXIT_TRACE_r10] = "_EXIT_TRACE_r10", + [_EXIT_TRACE_r20] = "_EXIT_TRACE_r20", + [_EXIT_TRACE_r30] = "_EXIT_TRACE_r30", [_EXPAND_METHOD] = "_EXPAND_METHOD", + [_EXPAND_METHOD_r00] = "_EXPAND_METHOD_r00", [_EXPAND_METHOD_KW] = "_EXPAND_METHOD_KW", + [_EXPAND_METHOD_KW_r11] = "_EXPAND_METHOD_KW_r11", [_FATAL_ERROR] = "_FATAL_ERROR", + [_FATAL_ERROR_r00] = "_FATAL_ERROR_r00", + [_FATAL_ERROR_r11] = "_FATAL_ERROR_r11", + [_FATAL_ERROR_r22] = "_FATAL_ERROR_r22", + [_FATAL_ERROR_r33] = "_FATAL_ERROR_r33", [_FORMAT_SIMPLE] = "_FORMAT_SIMPLE", + [_FORMAT_SIMPLE_r11] = "_FORMAT_SIMPLE_r11", [_FORMAT_WITH_SPEC] = "_FORMAT_WITH_SPEC", + [_FORMAT_WITH_SPEC_r21] = "_FORMAT_WITH_SPEC_r21", [_FOR_ITER_GEN_FRAME] = "_FOR_ITER_GEN_FRAME", + [_FOR_ITER_GEN_FRAME_r03] = "_FOR_ITER_GEN_FRAME_r03", + [_FOR_ITER_GEN_FRAME_r13] = "_FOR_ITER_GEN_FRAME_r13", + [_FOR_ITER_GEN_FRAME_r23] = "_FOR_ITER_GEN_FRAME_r23", [_FOR_ITER_TIER_TWO] = "_FOR_ITER_TIER_TWO", + [_FOR_ITER_TIER_TWO_r23] = "_FOR_ITER_TIER_TWO_r23", + [_FOR_ITER_VIRTUAL_TIER_TWO] = "_FOR_ITER_VIRTUAL_TIER_TWO", + [_FOR_ITER_VIRTUAL_TIER_TWO_r23] = "_FOR_ITER_VIRTUAL_TIER_TWO_r23", [_GET_AITER] = "_GET_AITER", + [_GET_AITER_r11] = "_GET_AITER_r11", [_GET_ANEXT] = "_GET_ANEXT", + [_GET_ANEXT_r12] = "_GET_ANEXT_r12", [_GET_AWAITABLE] = "_GET_AWAITABLE", + [_GET_AWAITABLE_r11] = "_GET_AWAITABLE_r11", [_GET_ITER] = "_GET_ITER", + [_GET_ITER_r12] = "_GET_ITER_r12", + [_GET_ITER_TRAD] = "_GET_ITER_TRAD", + [_GET_ITER_TRAD_r12] = "_GET_ITER_TRAD_r12", [_GET_LEN] = "_GET_LEN", - [_GET_YIELD_FROM_ITER] = "_GET_YIELD_FROM_ITER", + [_GET_LEN_r12] = "_GET_LEN_r12", + [_GUARD_3OS_ASYNC_GEN_ASEND] = "_GUARD_3OS_ASYNC_GEN_ASEND", + [_GUARD_3OS_ASYNC_GEN_ASEND_r03] = "_GUARD_3OS_ASYNC_GEN_ASEND_r03", + [_GUARD_3OS_ASYNC_GEN_ASEND_r13] = "_GUARD_3OS_ASYNC_GEN_ASEND_r13", + [_GUARD_3OS_ASYNC_GEN_ASEND_r23] = "_GUARD_3OS_ASYNC_GEN_ASEND_r23", + [_GUARD_3OS_ASYNC_GEN_ASEND_r33] = "_GUARD_3OS_ASYNC_GEN_ASEND_r33", [_GUARD_BINARY_OP_EXTEND] = "_GUARD_BINARY_OP_EXTEND", + [_GUARD_BINARY_OP_EXTEND_r22] = "_GUARD_BINARY_OP_EXTEND_r22", + [_GUARD_BINARY_OP_EXTEND_LHS] = "_GUARD_BINARY_OP_EXTEND_LHS", + [_GUARD_BINARY_OP_EXTEND_LHS_r02] = "_GUARD_BINARY_OP_EXTEND_LHS_r02", + [_GUARD_BINARY_OP_EXTEND_LHS_r12] = "_GUARD_BINARY_OP_EXTEND_LHS_r12", + [_GUARD_BINARY_OP_EXTEND_LHS_r22] = "_GUARD_BINARY_OP_EXTEND_LHS_r22", + [_GUARD_BINARY_OP_EXTEND_LHS_r33] = "_GUARD_BINARY_OP_EXTEND_LHS_r33", + [_GUARD_BINARY_OP_EXTEND_RHS] = "_GUARD_BINARY_OP_EXTEND_RHS", + [_GUARD_BINARY_OP_EXTEND_RHS_r02] = "_GUARD_BINARY_OP_EXTEND_RHS_r02", + [_GUARD_BINARY_OP_EXTEND_RHS_r12] = "_GUARD_BINARY_OP_EXTEND_RHS_r12", + [_GUARD_BINARY_OP_EXTEND_RHS_r22] = "_GUARD_BINARY_OP_EXTEND_RHS_r22", + [_GUARD_BINARY_OP_EXTEND_RHS_r33] = "_GUARD_BINARY_OP_EXTEND_RHS_r33", + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS] = "_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS", + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r02] = "_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r02", + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r12] = "_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r12", + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r22] = "_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r22", + [_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r33] = "_GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS_r33", + [_GUARD_BIT_IS_SET_POP] = "_GUARD_BIT_IS_SET_POP", + [_GUARD_BIT_IS_SET_POP_r00] = "_GUARD_BIT_IS_SET_POP_r00", + [_GUARD_BIT_IS_SET_POP_r10] = "_GUARD_BIT_IS_SET_POP_r10", + [_GUARD_BIT_IS_SET_POP_r21] = "_GUARD_BIT_IS_SET_POP_r21", + [_GUARD_BIT_IS_SET_POP_r32] = "_GUARD_BIT_IS_SET_POP_r32", + [_GUARD_BIT_IS_SET_POP_4] = "_GUARD_BIT_IS_SET_POP_4", + [_GUARD_BIT_IS_SET_POP_4_r00] = "_GUARD_BIT_IS_SET_POP_4_r00", + [_GUARD_BIT_IS_SET_POP_4_r10] = "_GUARD_BIT_IS_SET_POP_4_r10", + [_GUARD_BIT_IS_SET_POP_4_r21] = "_GUARD_BIT_IS_SET_POP_4_r21", + [_GUARD_BIT_IS_SET_POP_4_r32] = "_GUARD_BIT_IS_SET_POP_4_r32", + [_GUARD_BIT_IS_SET_POP_5] = "_GUARD_BIT_IS_SET_POP_5", + [_GUARD_BIT_IS_SET_POP_5_r00] = "_GUARD_BIT_IS_SET_POP_5_r00", + [_GUARD_BIT_IS_SET_POP_5_r10] = "_GUARD_BIT_IS_SET_POP_5_r10", + [_GUARD_BIT_IS_SET_POP_5_r21] = "_GUARD_BIT_IS_SET_POP_5_r21", + [_GUARD_BIT_IS_SET_POP_5_r32] = "_GUARD_BIT_IS_SET_POP_5_r32", + [_GUARD_BIT_IS_SET_POP_6] = "_GUARD_BIT_IS_SET_POP_6", + [_GUARD_BIT_IS_SET_POP_6_r00] = "_GUARD_BIT_IS_SET_POP_6_r00", + [_GUARD_BIT_IS_SET_POP_6_r10] = "_GUARD_BIT_IS_SET_POP_6_r10", + [_GUARD_BIT_IS_SET_POP_6_r21] = "_GUARD_BIT_IS_SET_POP_6_r21", + [_GUARD_BIT_IS_SET_POP_6_r32] = "_GUARD_BIT_IS_SET_POP_6_r32", + [_GUARD_BIT_IS_SET_POP_7] = "_GUARD_BIT_IS_SET_POP_7", + [_GUARD_BIT_IS_SET_POP_7_r00] = "_GUARD_BIT_IS_SET_POP_7_r00", + [_GUARD_BIT_IS_SET_POP_7_r10] = "_GUARD_BIT_IS_SET_POP_7_r10", + [_GUARD_BIT_IS_SET_POP_7_r21] = "_GUARD_BIT_IS_SET_POP_7_r21", + [_GUARD_BIT_IS_SET_POP_7_r32] = "_GUARD_BIT_IS_SET_POP_7_r32", + [_GUARD_BIT_IS_UNSET_POP] = "_GUARD_BIT_IS_UNSET_POP", + [_GUARD_BIT_IS_UNSET_POP_r00] = "_GUARD_BIT_IS_UNSET_POP_r00", + [_GUARD_BIT_IS_UNSET_POP_r10] = "_GUARD_BIT_IS_UNSET_POP_r10", + [_GUARD_BIT_IS_UNSET_POP_r21] = "_GUARD_BIT_IS_UNSET_POP_r21", + [_GUARD_BIT_IS_UNSET_POP_r32] = "_GUARD_BIT_IS_UNSET_POP_r32", + [_GUARD_BIT_IS_UNSET_POP_4] = "_GUARD_BIT_IS_UNSET_POP_4", + [_GUARD_BIT_IS_UNSET_POP_4_r00] = "_GUARD_BIT_IS_UNSET_POP_4_r00", + [_GUARD_BIT_IS_UNSET_POP_4_r10] = "_GUARD_BIT_IS_UNSET_POP_4_r10", + [_GUARD_BIT_IS_UNSET_POP_4_r21] = "_GUARD_BIT_IS_UNSET_POP_4_r21", + [_GUARD_BIT_IS_UNSET_POP_4_r32] = "_GUARD_BIT_IS_UNSET_POP_4_r32", + [_GUARD_BIT_IS_UNSET_POP_5] = "_GUARD_BIT_IS_UNSET_POP_5", + [_GUARD_BIT_IS_UNSET_POP_5_r00] = "_GUARD_BIT_IS_UNSET_POP_5_r00", + [_GUARD_BIT_IS_UNSET_POP_5_r10] = "_GUARD_BIT_IS_UNSET_POP_5_r10", + [_GUARD_BIT_IS_UNSET_POP_5_r21] = "_GUARD_BIT_IS_UNSET_POP_5_r21", + [_GUARD_BIT_IS_UNSET_POP_5_r32] = "_GUARD_BIT_IS_UNSET_POP_5_r32", + [_GUARD_BIT_IS_UNSET_POP_6] = "_GUARD_BIT_IS_UNSET_POP_6", + [_GUARD_BIT_IS_UNSET_POP_6_r00] = "_GUARD_BIT_IS_UNSET_POP_6_r00", + [_GUARD_BIT_IS_UNSET_POP_6_r10] = "_GUARD_BIT_IS_UNSET_POP_6_r10", + [_GUARD_BIT_IS_UNSET_POP_6_r21] = "_GUARD_BIT_IS_UNSET_POP_6_r21", + [_GUARD_BIT_IS_UNSET_POP_6_r32] = "_GUARD_BIT_IS_UNSET_POP_6_r32", + [_GUARD_BIT_IS_UNSET_POP_7] = "_GUARD_BIT_IS_UNSET_POP_7", + [_GUARD_BIT_IS_UNSET_POP_7_r00] = "_GUARD_BIT_IS_UNSET_POP_7_r00", + [_GUARD_BIT_IS_UNSET_POP_7_r10] = "_GUARD_BIT_IS_UNSET_POP_7_r10", + [_GUARD_BIT_IS_UNSET_POP_7_r21] = "_GUARD_BIT_IS_UNSET_POP_7_r21", + [_GUARD_BIT_IS_UNSET_POP_7_r32] = "_GUARD_BIT_IS_UNSET_POP_7_r32", + [_GUARD_CALLABLE_BUILTIN_CLASS] = "_GUARD_CALLABLE_BUILTIN_CLASS", + [_GUARD_CALLABLE_BUILTIN_CLASS_r00] = "_GUARD_CALLABLE_BUILTIN_CLASS_r00", + [_GUARD_CALLABLE_BUILTIN_FAST] = "_GUARD_CALLABLE_BUILTIN_FAST", + [_GUARD_CALLABLE_BUILTIN_FAST_r00] = "_GUARD_CALLABLE_BUILTIN_FAST_r00", + [_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS] = "_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS", + [_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS_r00] = "_GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS_r00", + [_GUARD_CALLABLE_BUILTIN_O] = "_GUARD_CALLABLE_BUILTIN_O", + [_GUARD_CALLABLE_BUILTIN_O_r00] = "_GUARD_CALLABLE_BUILTIN_O_r00", + [_GUARD_CALLABLE_ISINSTANCE] = "_GUARD_CALLABLE_ISINSTANCE", + [_GUARD_CALLABLE_ISINSTANCE_r03] = "_GUARD_CALLABLE_ISINSTANCE_r03", + [_GUARD_CALLABLE_ISINSTANCE_r13] = "_GUARD_CALLABLE_ISINSTANCE_r13", + [_GUARD_CALLABLE_ISINSTANCE_r23] = "_GUARD_CALLABLE_ISINSTANCE_r23", + [_GUARD_CALLABLE_ISINSTANCE_r33] = "_GUARD_CALLABLE_ISINSTANCE_r33", [_GUARD_CALLABLE_LEN] = "_GUARD_CALLABLE_LEN", + [_GUARD_CALLABLE_LEN_r03] = "_GUARD_CALLABLE_LEN_r03", + [_GUARD_CALLABLE_LEN_r13] = "_GUARD_CALLABLE_LEN_r13", + [_GUARD_CALLABLE_LEN_r23] = "_GUARD_CALLABLE_LEN_r23", + [_GUARD_CALLABLE_LEN_r33] = "_GUARD_CALLABLE_LEN_r33", + [_GUARD_CALLABLE_LIST_APPEND] = "_GUARD_CALLABLE_LIST_APPEND", + [_GUARD_CALLABLE_LIST_APPEND_r03] = "_GUARD_CALLABLE_LIST_APPEND_r03", + [_GUARD_CALLABLE_LIST_APPEND_r13] = "_GUARD_CALLABLE_LIST_APPEND_r13", + [_GUARD_CALLABLE_LIST_APPEND_r23] = "_GUARD_CALLABLE_LIST_APPEND_r23", + [_GUARD_CALLABLE_LIST_APPEND_r33] = "_GUARD_CALLABLE_LIST_APPEND_r33", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_r00] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_r00", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_r00", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS_r00] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS_r00", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_O] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_O", + [_GUARD_CALLABLE_METHOD_DESCRIPTOR_O_r00] = "_GUARD_CALLABLE_METHOD_DESCRIPTOR_O_r00", [_GUARD_CALLABLE_STR_1] = "_GUARD_CALLABLE_STR_1", + [_GUARD_CALLABLE_STR_1_r03] = "_GUARD_CALLABLE_STR_1_r03", + [_GUARD_CALLABLE_STR_1_r13] = "_GUARD_CALLABLE_STR_1_r13", + [_GUARD_CALLABLE_STR_1_r23] = "_GUARD_CALLABLE_STR_1_r23", + [_GUARD_CALLABLE_STR_1_r33] = "_GUARD_CALLABLE_STR_1_r33", [_GUARD_CALLABLE_TUPLE_1] = "_GUARD_CALLABLE_TUPLE_1", + [_GUARD_CALLABLE_TUPLE_1_r03] = "_GUARD_CALLABLE_TUPLE_1_r03", + [_GUARD_CALLABLE_TUPLE_1_r13] = "_GUARD_CALLABLE_TUPLE_1_r13", + [_GUARD_CALLABLE_TUPLE_1_r23] = "_GUARD_CALLABLE_TUPLE_1_r23", + [_GUARD_CALLABLE_TUPLE_1_r33] = "_GUARD_CALLABLE_TUPLE_1_r33", [_GUARD_CALLABLE_TYPE_1] = "_GUARD_CALLABLE_TYPE_1", + [_GUARD_CALLABLE_TYPE_1_r03] = "_GUARD_CALLABLE_TYPE_1_r03", + [_GUARD_CALLABLE_TYPE_1_r13] = "_GUARD_CALLABLE_TYPE_1_r13", + [_GUARD_CALLABLE_TYPE_1_r23] = "_GUARD_CALLABLE_TYPE_1_r23", + [_GUARD_CALLABLE_TYPE_1_r33] = "_GUARD_CALLABLE_TYPE_1_r33", + [_GUARD_CODE_VERSION_RETURN_GENERATOR] = "_GUARD_CODE_VERSION_RETURN_GENERATOR", + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r00] = "_GUARD_CODE_VERSION_RETURN_GENERATOR_r00", + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r11] = "_GUARD_CODE_VERSION_RETURN_GENERATOR_r11", + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r22] = "_GUARD_CODE_VERSION_RETURN_GENERATOR_r22", + [_GUARD_CODE_VERSION_RETURN_GENERATOR_r33] = "_GUARD_CODE_VERSION_RETURN_GENERATOR_r33", + [_GUARD_CODE_VERSION_RETURN_VALUE] = "_GUARD_CODE_VERSION_RETURN_VALUE", + [_GUARD_CODE_VERSION_RETURN_VALUE_r00] = "_GUARD_CODE_VERSION_RETURN_VALUE_r00", + [_GUARD_CODE_VERSION_RETURN_VALUE_r11] = "_GUARD_CODE_VERSION_RETURN_VALUE_r11", + [_GUARD_CODE_VERSION_RETURN_VALUE_r22] = "_GUARD_CODE_VERSION_RETURN_VALUE_r22", + [_GUARD_CODE_VERSION_RETURN_VALUE_r33] = "_GUARD_CODE_VERSION_RETURN_VALUE_r33", + [_GUARD_CODE_VERSION_YIELD_VALUE] = "_GUARD_CODE_VERSION_YIELD_VALUE", + [_GUARD_CODE_VERSION_YIELD_VALUE_r00] = "_GUARD_CODE_VERSION_YIELD_VALUE_r00", + [_GUARD_CODE_VERSION_YIELD_VALUE_r11] = "_GUARD_CODE_VERSION_YIELD_VALUE_r11", + [_GUARD_CODE_VERSION_YIELD_VALUE_r22] = "_GUARD_CODE_VERSION_YIELD_VALUE_r22", + [_GUARD_CODE_VERSION_YIELD_VALUE_r33] = "_GUARD_CODE_VERSION_YIELD_VALUE_r33", + [_GUARD_CODE_VERSION__PUSH_FRAME] = "_GUARD_CODE_VERSION__PUSH_FRAME", + [_GUARD_CODE_VERSION__PUSH_FRAME_r00] = "_GUARD_CODE_VERSION__PUSH_FRAME_r00", + [_GUARD_CODE_VERSION__PUSH_FRAME_r11] = "_GUARD_CODE_VERSION__PUSH_FRAME_r11", + [_GUARD_CODE_VERSION__PUSH_FRAME_r22] = "_GUARD_CODE_VERSION__PUSH_FRAME_r22", + [_GUARD_CODE_VERSION__PUSH_FRAME_r33] = "_GUARD_CODE_VERSION__PUSH_FRAME_r33", [_GUARD_DORV_NO_DICT] = "_GUARD_DORV_NO_DICT", + [_GUARD_DORV_NO_DICT_r01] = "_GUARD_DORV_NO_DICT_r01", + [_GUARD_DORV_NO_DICT_r11] = "_GUARD_DORV_NO_DICT_r11", + [_GUARD_DORV_NO_DICT_r22] = "_GUARD_DORV_NO_DICT_r22", + [_GUARD_DORV_NO_DICT_r33] = "_GUARD_DORV_NO_DICT_r33", [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT] = "_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT", + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r01] = "_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r01", + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r11] = "_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r11", + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r22] = "_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r22", + [_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r33] = "_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT_r33", [_GUARD_GLOBALS_VERSION] = "_GUARD_GLOBALS_VERSION", + [_GUARD_GLOBALS_VERSION_r00] = "_GUARD_GLOBALS_VERSION_r00", + [_GUARD_GLOBALS_VERSION_r11] = "_GUARD_GLOBALS_VERSION_r11", + [_GUARD_GLOBALS_VERSION_r22] = "_GUARD_GLOBALS_VERSION_r22", + [_GUARD_GLOBALS_VERSION_r33] = "_GUARD_GLOBALS_VERSION_r33", + [_GUARD_IP_RETURN_GENERATOR] = "_GUARD_IP_RETURN_GENERATOR", + [_GUARD_IP_RETURN_GENERATOR_r00] = "_GUARD_IP_RETURN_GENERATOR_r00", + [_GUARD_IP_RETURN_GENERATOR_r11] = "_GUARD_IP_RETURN_GENERATOR_r11", + [_GUARD_IP_RETURN_GENERATOR_r22] = "_GUARD_IP_RETURN_GENERATOR_r22", + [_GUARD_IP_RETURN_GENERATOR_r33] = "_GUARD_IP_RETURN_GENERATOR_r33", + [_GUARD_IP_RETURN_VALUE] = "_GUARD_IP_RETURN_VALUE", + [_GUARD_IP_RETURN_VALUE_r00] = "_GUARD_IP_RETURN_VALUE_r00", + [_GUARD_IP_RETURN_VALUE_r11] = "_GUARD_IP_RETURN_VALUE_r11", + [_GUARD_IP_RETURN_VALUE_r22] = "_GUARD_IP_RETURN_VALUE_r22", + [_GUARD_IP_RETURN_VALUE_r33] = "_GUARD_IP_RETURN_VALUE_r33", + [_GUARD_IP_YIELD_VALUE] = "_GUARD_IP_YIELD_VALUE", + [_GUARD_IP_YIELD_VALUE_r00] = "_GUARD_IP_YIELD_VALUE_r00", + [_GUARD_IP_YIELD_VALUE_r11] = "_GUARD_IP_YIELD_VALUE_r11", + [_GUARD_IP_YIELD_VALUE_r22] = "_GUARD_IP_YIELD_VALUE_r22", + [_GUARD_IP_YIELD_VALUE_r33] = "_GUARD_IP_YIELD_VALUE_r33", + [_GUARD_IP__PUSH_FRAME] = "_GUARD_IP__PUSH_FRAME", + [_GUARD_IP__PUSH_FRAME_r00] = "_GUARD_IP__PUSH_FRAME_r00", + [_GUARD_IP__PUSH_FRAME_r11] = "_GUARD_IP__PUSH_FRAME_r11", + [_GUARD_IP__PUSH_FRAME_r22] = "_GUARD_IP__PUSH_FRAME_r22", + [_GUARD_IP__PUSH_FRAME_r33] = "_GUARD_IP__PUSH_FRAME_r33", [_GUARD_IS_FALSE_POP] = "_GUARD_IS_FALSE_POP", + [_GUARD_IS_FALSE_POP_r00] = "_GUARD_IS_FALSE_POP_r00", + [_GUARD_IS_FALSE_POP_r10] = "_GUARD_IS_FALSE_POP_r10", + [_GUARD_IS_FALSE_POP_r21] = "_GUARD_IS_FALSE_POP_r21", + [_GUARD_IS_FALSE_POP_r32] = "_GUARD_IS_FALSE_POP_r32", [_GUARD_IS_NONE_POP] = "_GUARD_IS_NONE_POP", + [_GUARD_IS_NONE_POP_r00] = "_GUARD_IS_NONE_POP_r00", + [_GUARD_IS_NONE_POP_r10] = "_GUARD_IS_NONE_POP_r10", + [_GUARD_IS_NONE_POP_r21] = "_GUARD_IS_NONE_POP_r21", + [_GUARD_IS_NONE_POP_r32] = "_GUARD_IS_NONE_POP_r32", [_GUARD_IS_NOT_NONE_POP] = "_GUARD_IS_NOT_NONE_POP", + [_GUARD_IS_NOT_NONE_POP_r10] = "_GUARD_IS_NOT_NONE_POP_r10", [_GUARD_IS_TRUE_POP] = "_GUARD_IS_TRUE_POP", + [_GUARD_IS_TRUE_POP_r00] = "_GUARD_IS_TRUE_POP_r00", + [_GUARD_IS_TRUE_POP_r10] = "_GUARD_IS_TRUE_POP_r10", + [_GUARD_IS_TRUE_POP_r21] = "_GUARD_IS_TRUE_POP_r21", + [_GUARD_IS_TRUE_POP_r32] = "_GUARD_IS_TRUE_POP_r32", + [_GUARD_ITERATOR] = "_GUARD_ITERATOR", + [_GUARD_ITERATOR_r01] = "_GUARD_ITERATOR_r01", + [_GUARD_ITERATOR_r11] = "_GUARD_ITERATOR_r11", + [_GUARD_ITERATOR_r22] = "_GUARD_ITERATOR_r22", + [_GUARD_ITERATOR_r33] = "_GUARD_ITERATOR_r33", + [_GUARD_ITER_VIRTUAL] = "_GUARD_ITER_VIRTUAL", + [_GUARD_ITER_VIRTUAL_r01] = "_GUARD_ITER_VIRTUAL_r01", + [_GUARD_ITER_VIRTUAL_r11] = "_GUARD_ITER_VIRTUAL_r11", + [_GUARD_ITER_VIRTUAL_r22] = "_GUARD_ITER_VIRTUAL_r22", + [_GUARD_ITER_VIRTUAL_r33] = "_GUARD_ITER_VIRTUAL_r33", [_GUARD_KEYS_VERSION] = "_GUARD_KEYS_VERSION", - [_GUARD_NOS_DICT] = "_GUARD_NOS_DICT", + [_GUARD_KEYS_VERSION_r01] = "_GUARD_KEYS_VERSION_r01", + [_GUARD_KEYS_VERSION_r11] = "_GUARD_KEYS_VERSION_r11", + [_GUARD_KEYS_VERSION_r22] = "_GUARD_KEYS_VERSION_r22", + [_GUARD_KEYS_VERSION_r33] = "_GUARD_KEYS_VERSION_r33", + [_GUARD_LOAD_SUPER_ATTR_METHOD] = "_GUARD_LOAD_SUPER_ATTR_METHOD", + [_GUARD_LOAD_SUPER_ATTR_METHOD_r03] = "_GUARD_LOAD_SUPER_ATTR_METHOD_r03", + [_GUARD_LOAD_SUPER_ATTR_METHOD_r13] = "_GUARD_LOAD_SUPER_ATTR_METHOD_r13", + [_GUARD_LOAD_SUPER_ATTR_METHOD_r23] = "_GUARD_LOAD_SUPER_ATTR_METHOD_r23", + [_GUARD_LOAD_SUPER_ATTR_METHOD_r33] = "_GUARD_LOAD_SUPER_ATTR_METHOD_r33", + [_GUARD_NOS_COMPACT_ASCII] = "_GUARD_NOS_COMPACT_ASCII", + [_GUARD_NOS_COMPACT_ASCII_r02] = "_GUARD_NOS_COMPACT_ASCII_r02", + [_GUARD_NOS_COMPACT_ASCII_r12] = "_GUARD_NOS_COMPACT_ASCII_r12", + [_GUARD_NOS_COMPACT_ASCII_r22] = "_GUARD_NOS_COMPACT_ASCII_r22", + [_GUARD_NOS_COMPACT_ASCII_r33] = "_GUARD_NOS_COMPACT_ASCII_r33", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r03] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r03", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r13] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r13", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r23] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r23", + [_GUARD_NOS_DICT_STORE_SUBSCRIPT_r33] = "_GUARD_NOS_DICT_STORE_SUBSCRIPT_r33", + [_GUARD_NOS_DICT_SUBSCRIPT] = "_GUARD_NOS_DICT_SUBSCRIPT", + [_GUARD_NOS_DICT_SUBSCRIPT_r02] = "_GUARD_NOS_DICT_SUBSCRIPT_r02", + [_GUARD_NOS_DICT_SUBSCRIPT_r12] = "_GUARD_NOS_DICT_SUBSCRIPT_r12", + [_GUARD_NOS_DICT_SUBSCRIPT_r22] = "_GUARD_NOS_DICT_SUBSCRIPT_r22", + [_GUARD_NOS_DICT_SUBSCRIPT_r33] = "_GUARD_NOS_DICT_SUBSCRIPT_r33", [_GUARD_NOS_FLOAT] = "_GUARD_NOS_FLOAT", + [_GUARD_NOS_FLOAT_r02] = "_GUARD_NOS_FLOAT_r02", + [_GUARD_NOS_FLOAT_r12] = "_GUARD_NOS_FLOAT_r12", + [_GUARD_NOS_FLOAT_r22] = "_GUARD_NOS_FLOAT_r22", + [_GUARD_NOS_FLOAT_r33] = "_GUARD_NOS_FLOAT_r33", [_GUARD_NOS_INT] = "_GUARD_NOS_INT", + [_GUARD_NOS_INT_r02] = "_GUARD_NOS_INT_r02", + [_GUARD_NOS_INT_r12] = "_GUARD_NOS_INT_r12", + [_GUARD_NOS_INT_r22] = "_GUARD_NOS_INT_r22", + [_GUARD_NOS_INT_r33] = "_GUARD_NOS_INT_r33", + [_GUARD_NOS_ITER_VIRTUAL] = "_GUARD_NOS_ITER_VIRTUAL", + [_GUARD_NOS_ITER_VIRTUAL_r02] = "_GUARD_NOS_ITER_VIRTUAL_r02", + [_GUARD_NOS_ITER_VIRTUAL_r12] = "_GUARD_NOS_ITER_VIRTUAL_r12", + [_GUARD_NOS_ITER_VIRTUAL_r22] = "_GUARD_NOS_ITER_VIRTUAL_r22", + [_GUARD_NOS_ITER_VIRTUAL_r33] = "_GUARD_NOS_ITER_VIRTUAL_r33", [_GUARD_NOS_LIST] = "_GUARD_NOS_LIST", + [_GUARD_NOS_LIST_r02] = "_GUARD_NOS_LIST_r02", + [_GUARD_NOS_LIST_r12] = "_GUARD_NOS_LIST_r12", + [_GUARD_NOS_LIST_r22] = "_GUARD_NOS_LIST_r22", + [_GUARD_NOS_LIST_r33] = "_GUARD_NOS_LIST_r33", + [_GUARD_NOS_NOT_NULL] = "_GUARD_NOS_NOT_NULL", + [_GUARD_NOS_NOT_NULL_r02] = "_GUARD_NOS_NOT_NULL_r02", + [_GUARD_NOS_NOT_NULL_r12] = "_GUARD_NOS_NOT_NULL_r12", + [_GUARD_NOS_NOT_NULL_r22] = "_GUARD_NOS_NOT_NULL_r22", + [_GUARD_NOS_NOT_NULL_r33] = "_GUARD_NOS_NOT_NULL_r33", [_GUARD_NOS_NULL] = "_GUARD_NOS_NULL", + [_GUARD_NOS_NULL_r02] = "_GUARD_NOS_NULL_r02", + [_GUARD_NOS_NULL_r12] = "_GUARD_NOS_NULL_r12", + [_GUARD_NOS_NULL_r22] = "_GUARD_NOS_NULL_r22", + [_GUARD_NOS_NULL_r33] = "_GUARD_NOS_NULL_r33", + [_GUARD_NOS_OVERFLOWED] = "_GUARD_NOS_OVERFLOWED", + [_GUARD_NOS_OVERFLOWED_r02] = "_GUARD_NOS_OVERFLOWED_r02", + [_GUARD_NOS_OVERFLOWED_r12] = "_GUARD_NOS_OVERFLOWED_r12", + [_GUARD_NOS_OVERFLOWED_r22] = "_GUARD_NOS_OVERFLOWED_r22", + [_GUARD_NOS_OVERFLOWED_r33] = "_GUARD_NOS_OVERFLOWED_r33", [_GUARD_NOS_TUPLE] = "_GUARD_NOS_TUPLE", + [_GUARD_NOS_TUPLE_r02] = "_GUARD_NOS_TUPLE_r02", + [_GUARD_NOS_TUPLE_r12] = "_GUARD_NOS_TUPLE_r12", + [_GUARD_NOS_TUPLE_r22] = "_GUARD_NOS_TUPLE_r22", + [_GUARD_NOS_TUPLE_r33] = "_GUARD_NOS_TUPLE_r33", + [_GUARD_NOS_TYPE_VERSION] = "_GUARD_NOS_TYPE_VERSION", + [_GUARD_NOS_TYPE_VERSION_r02] = "_GUARD_NOS_TYPE_VERSION_r02", + [_GUARD_NOS_TYPE_VERSION_r12] = "_GUARD_NOS_TYPE_VERSION_r12", + [_GUARD_NOS_TYPE_VERSION_r22] = "_GUARD_NOS_TYPE_VERSION_r22", + [_GUARD_NOS_TYPE_VERSION_r33] = "_GUARD_NOS_TYPE_VERSION_r33", [_GUARD_NOS_UNICODE] = "_GUARD_NOS_UNICODE", + [_GUARD_NOS_UNICODE_r02] = "_GUARD_NOS_UNICODE_r02", + [_GUARD_NOS_UNICODE_r12] = "_GUARD_NOS_UNICODE_r12", + [_GUARD_NOS_UNICODE_r22] = "_GUARD_NOS_UNICODE_r22", + [_GUARD_NOS_UNICODE_r33] = "_GUARD_NOS_UNICODE_r33", [_GUARD_NOT_EXHAUSTED_LIST] = "_GUARD_NOT_EXHAUSTED_LIST", + [_GUARD_NOT_EXHAUSTED_LIST_r02] = "_GUARD_NOT_EXHAUSTED_LIST_r02", + [_GUARD_NOT_EXHAUSTED_LIST_r12] = "_GUARD_NOT_EXHAUSTED_LIST_r12", + [_GUARD_NOT_EXHAUSTED_LIST_r22] = "_GUARD_NOT_EXHAUSTED_LIST_r22", + [_GUARD_NOT_EXHAUSTED_LIST_r33] = "_GUARD_NOT_EXHAUSTED_LIST_r33", [_GUARD_NOT_EXHAUSTED_RANGE] = "_GUARD_NOT_EXHAUSTED_RANGE", + [_GUARD_NOT_EXHAUSTED_RANGE_r02] = "_GUARD_NOT_EXHAUSTED_RANGE_r02", + [_GUARD_NOT_EXHAUSTED_RANGE_r12] = "_GUARD_NOT_EXHAUSTED_RANGE_r12", + [_GUARD_NOT_EXHAUSTED_RANGE_r22] = "_GUARD_NOT_EXHAUSTED_RANGE_r22", + [_GUARD_NOT_EXHAUSTED_RANGE_r33] = "_GUARD_NOT_EXHAUSTED_RANGE_r33", [_GUARD_NOT_EXHAUSTED_TUPLE] = "_GUARD_NOT_EXHAUSTED_TUPLE", + [_GUARD_NOT_EXHAUSTED_TUPLE_r02] = "_GUARD_NOT_EXHAUSTED_TUPLE_r02", + [_GUARD_NOT_EXHAUSTED_TUPLE_r12] = "_GUARD_NOT_EXHAUSTED_TUPLE_r12", + [_GUARD_NOT_EXHAUSTED_TUPLE_r22] = "_GUARD_NOT_EXHAUSTED_TUPLE_r22", + [_GUARD_NOT_EXHAUSTED_TUPLE_r33] = "_GUARD_NOT_EXHAUSTED_TUPLE_r33", + [_GUARD_THIRD_NULL] = "_GUARD_THIRD_NULL", + [_GUARD_THIRD_NULL_r03] = "_GUARD_THIRD_NULL_r03", + [_GUARD_THIRD_NULL_r13] = "_GUARD_THIRD_NULL_r13", + [_GUARD_THIRD_NULL_r23] = "_GUARD_THIRD_NULL_r23", + [_GUARD_THIRD_NULL_r33] = "_GUARD_THIRD_NULL_r33", + [_GUARD_TOS_ANY_DICT] = "_GUARD_TOS_ANY_DICT", + [_GUARD_TOS_ANY_DICT_r01] = "_GUARD_TOS_ANY_DICT_r01", + [_GUARD_TOS_ANY_DICT_r11] = "_GUARD_TOS_ANY_DICT_r11", + [_GUARD_TOS_ANY_DICT_r22] = "_GUARD_TOS_ANY_DICT_r22", + [_GUARD_TOS_ANY_DICT_r33] = "_GUARD_TOS_ANY_DICT_r33", [_GUARD_TOS_ANY_SET] = "_GUARD_TOS_ANY_SET", + [_GUARD_TOS_ANY_SET_r01] = "_GUARD_TOS_ANY_SET_r01", + [_GUARD_TOS_ANY_SET_r11] = "_GUARD_TOS_ANY_SET_r11", + [_GUARD_TOS_ANY_SET_r22] = "_GUARD_TOS_ANY_SET_r22", + [_GUARD_TOS_ANY_SET_r33] = "_GUARD_TOS_ANY_SET_r33", [_GUARD_TOS_DICT] = "_GUARD_TOS_DICT", + [_GUARD_TOS_DICT_r01] = "_GUARD_TOS_DICT_r01", + [_GUARD_TOS_DICT_r11] = "_GUARD_TOS_DICT_r11", + [_GUARD_TOS_DICT_r22] = "_GUARD_TOS_DICT_r22", + [_GUARD_TOS_DICT_r33] = "_GUARD_TOS_DICT_r33", [_GUARD_TOS_FLOAT] = "_GUARD_TOS_FLOAT", + [_GUARD_TOS_FLOAT_r01] = "_GUARD_TOS_FLOAT_r01", + [_GUARD_TOS_FLOAT_r11] = "_GUARD_TOS_FLOAT_r11", + [_GUARD_TOS_FLOAT_r22] = "_GUARD_TOS_FLOAT_r22", + [_GUARD_TOS_FLOAT_r33] = "_GUARD_TOS_FLOAT_r33", + [_GUARD_TOS_FROZENDICT] = "_GUARD_TOS_FROZENDICT", + [_GUARD_TOS_FROZENDICT_r01] = "_GUARD_TOS_FROZENDICT_r01", + [_GUARD_TOS_FROZENDICT_r11] = "_GUARD_TOS_FROZENDICT_r11", + [_GUARD_TOS_FROZENDICT_r22] = "_GUARD_TOS_FROZENDICT_r22", + [_GUARD_TOS_FROZENDICT_r33] = "_GUARD_TOS_FROZENDICT_r33", + [_GUARD_TOS_FROZENSET] = "_GUARD_TOS_FROZENSET", + [_GUARD_TOS_FROZENSET_r01] = "_GUARD_TOS_FROZENSET_r01", + [_GUARD_TOS_FROZENSET_r11] = "_GUARD_TOS_FROZENSET_r11", + [_GUARD_TOS_FROZENSET_r22] = "_GUARD_TOS_FROZENSET_r22", + [_GUARD_TOS_FROZENSET_r33] = "_GUARD_TOS_FROZENSET_r33", [_GUARD_TOS_INT] = "_GUARD_TOS_INT", + [_GUARD_TOS_INT_r01] = "_GUARD_TOS_INT_r01", + [_GUARD_TOS_INT_r11] = "_GUARD_TOS_INT_r11", + [_GUARD_TOS_INT_r22] = "_GUARD_TOS_INT_r22", + [_GUARD_TOS_INT_r33] = "_GUARD_TOS_INT_r33", + [_GUARD_TOS_IS_NONE] = "_GUARD_TOS_IS_NONE", + [_GUARD_TOS_IS_NONE_r01] = "_GUARD_TOS_IS_NONE_r01", + [_GUARD_TOS_IS_NONE_r11] = "_GUARD_TOS_IS_NONE_r11", + [_GUARD_TOS_IS_NONE_r22] = "_GUARD_TOS_IS_NONE_r22", + [_GUARD_TOS_IS_NONE_r33] = "_GUARD_TOS_IS_NONE_r33", [_GUARD_TOS_LIST] = "_GUARD_TOS_LIST", + [_GUARD_TOS_LIST_r01] = "_GUARD_TOS_LIST_r01", + [_GUARD_TOS_LIST_r11] = "_GUARD_TOS_LIST_r11", + [_GUARD_TOS_LIST_r22] = "_GUARD_TOS_LIST_r22", + [_GUARD_TOS_LIST_r33] = "_GUARD_TOS_LIST_r33", + [_GUARD_TOS_NOT_NULL] = "_GUARD_TOS_NOT_NULL", + [_GUARD_TOS_NOT_NULL_r01] = "_GUARD_TOS_NOT_NULL_r01", + [_GUARD_TOS_NOT_NULL_r11] = "_GUARD_TOS_NOT_NULL_r11", + [_GUARD_TOS_NOT_NULL_r22] = "_GUARD_TOS_NOT_NULL_r22", + [_GUARD_TOS_NOT_NULL_r33] = "_GUARD_TOS_NOT_NULL_r33", + [_GUARD_TOS_OVERFLOWED] = "_GUARD_TOS_OVERFLOWED", + [_GUARD_TOS_OVERFLOWED_r01] = "_GUARD_TOS_OVERFLOWED_r01", + [_GUARD_TOS_OVERFLOWED_r11] = "_GUARD_TOS_OVERFLOWED_r11", + [_GUARD_TOS_OVERFLOWED_r22] = "_GUARD_TOS_OVERFLOWED_r22", + [_GUARD_TOS_OVERFLOWED_r33] = "_GUARD_TOS_OVERFLOWED_r33", + [_GUARD_TOS_SET] = "_GUARD_TOS_SET", + [_GUARD_TOS_SET_r01] = "_GUARD_TOS_SET_r01", + [_GUARD_TOS_SET_r11] = "_GUARD_TOS_SET_r11", + [_GUARD_TOS_SET_r22] = "_GUARD_TOS_SET_r22", + [_GUARD_TOS_SET_r33] = "_GUARD_TOS_SET_r33", [_GUARD_TOS_SLICE] = "_GUARD_TOS_SLICE", + [_GUARD_TOS_SLICE_r01] = "_GUARD_TOS_SLICE_r01", + [_GUARD_TOS_SLICE_r11] = "_GUARD_TOS_SLICE_r11", + [_GUARD_TOS_SLICE_r22] = "_GUARD_TOS_SLICE_r22", + [_GUARD_TOS_SLICE_r33] = "_GUARD_TOS_SLICE_r33", [_GUARD_TOS_TUPLE] = "_GUARD_TOS_TUPLE", + [_GUARD_TOS_TUPLE_r01] = "_GUARD_TOS_TUPLE_r01", + [_GUARD_TOS_TUPLE_r11] = "_GUARD_TOS_TUPLE_r11", + [_GUARD_TOS_TUPLE_r22] = "_GUARD_TOS_TUPLE_r22", + [_GUARD_TOS_TUPLE_r33] = "_GUARD_TOS_TUPLE_r33", [_GUARD_TOS_UNICODE] = "_GUARD_TOS_UNICODE", + [_GUARD_TOS_UNICODE_r01] = "_GUARD_TOS_UNICODE_r01", + [_GUARD_TOS_UNICODE_r11] = "_GUARD_TOS_UNICODE_r11", + [_GUARD_TOS_UNICODE_r22] = "_GUARD_TOS_UNICODE_r22", + [_GUARD_TOS_UNICODE_r33] = "_GUARD_TOS_UNICODE_r33", + [_GUARD_TYPE] = "_GUARD_TYPE", + [_GUARD_TYPE_r01] = "_GUARD_TYPE_r01", + [_GUARD_TYPE_r11] = "_GUARD_TYPE_r11", + [_GUARD_TYPE_r22] = "_GUARD_TYPE_r22", + [_GUARD_TYPE_r33] = "_GUARD_TYPE_r33", + [_GUARD_TYPE_ITER] = "_GUARD_TYPE_ITER", + [_GUARD_TYPE_ITER_r02] = "_GUARD_TYPE_ITER_r02", + [_GUARD_TYPE_ITER_r12] = "_GUARD_TYPE_ITER_r12", + [_GUARD_TYPE_ITER_r22] = "_GUARD_TYPE_ITER_r22", + [_GUARD_TYPE_ITER_r33] = "_GUARD_TYPE_ITER_r33", [_GUARD_TYPE_VERSION] = "_GUARD_TYPE_VERSION", - [_GUARD_TYPE_VERSION_AND_LOCK] = "_GUARD_TYPE_VERSION_AND_LOCK", + [_GUARD_TYPE_VERSION_r01] = "_GUARD_TYPE_VERSION_r01", + [_GUARD_TYPE_VERSION_r11] = "_GUARD_TYPE_VERSION_r11", + [_GUARD_TYPE_VERSION_r22] = "_GUARD_TYPE_VERSION_r22", + [_GUARD_TYPE_VERSION_r33] = "_GUARD_TYPE_VERSION_r33", + [_GUARD_TYPE_VERSION_LOCKED] = "_GUARD_TYPE_VERSION_LOCKED", + [_GUARD_TYPE_VERSION_LOCKED_r01] = "_GUARD_TYPE_VERSION_LOCKED_r01", + [_GUARD_TYPE_VERSION_LOCKED_r11] = "_GUARD_TYPE_VERSION_LOCKED_r11", + [_GUARD_TYPE_VERSION_LOCKED_r22] = "_GUARD_TYPE_VERSION_LOCKED_r22", + [_GUARD_TYPE_VERSION_LOCKED_r33] = "_GUARD_TYPE_VERSION_LOCKED_r33", + [_HANDLE_PENDING_AND_DEOPT] = "_HANDLE_PENDING_AND_DEOPT", + [_HANDLE_PENDING_AND_DEOPT_r00] = "_HANDLE_PENDING_AND_DEOPT_r00", + [_HANDLE_PENDING_AND_DEOPT_r10] = "_HANDLE_PENDING_AND_DEOPT_r10", + [_HANDLE_PENDING_AND_DEOPT_r20] = "_HANDLE_PENDING_AND_DEOPT_r20", + [_HANDLE_PENDING_AND_DEOPT_r30] = "_HANDLE_PENDING_AND_DEOPT_r30", [_IMPORT_FROM] = "_IMPORT_FROM", + [_IMPORT_FROM_r12] = "_IMPORT_FROM_r12", [_IMPORT_NAME] = "_IMPORT_NAME", + [_IMPORT_NAME_r21] = "_IMPORT_NAME_r21", [_INIT_CALL_BOUND_METHOD_EXACT_ARGS] = "_INIT_CALL_BOUND_METHOD_EXACT_ARGS", + [_INIT_CALL_BOUND_METHOD_EXACT_ARGS_r00] = "_INIT_CALL_BOUND_METHOD_EXACT_ARGS_r00", [_INIT_CALL_PY_EXACT_ARGS] = "_INIT_CALL_PY_EXACT_ARGS", + [_INIT_CALL_PY_EXACT_ARGS_r01] = "_INIT_CALL_PY_EXACT_ARGS_r01", [_INIT_CALL_PY_EXACT_ARGS_0] = "_INIT_CALL_PY_EXACT_ARGS_0", + [_INIT_CALL_PY_EXACT_ARGS_0_r01] = "_INIT_CALL_PY_EXACT_ARGS_0_r01", [_INIT_CALL_PY_EXACT_ARGS_1] = "_INIT_CALL_PY_EXACT_ARGS_1", + [_INIT_CALL_PY_EXACT_ARGS_1_r01] = "_INIT_CALL_PY_EXACT_ARGS_1_r01", [_INIT_CALL_PY_EXACT_ARGS_2] = "_INIT_CALL_PY_EXACT_ARGS_2", + [_INIT_CALL_PY_EXACT_ARGS_2_r01] = "_INIT_CALL_PY_EXACT_ARGS_2_r01", [_INIT_CALL_PY_EXACT_ARGS_3] = "_INIT_CALL_PY_EXACT_ARGS_3", + [_INIT_CALL_PY_EXACT_ARGS_3_r01] = "_INIT_CALL_PY_EXACT_ARGS_3_r01", [_INIT_CALL_PY_EXACT_ARGS_4] = "_INIT_CALL_PY_EXACT_ARGS_4", + [_INIT_CALL_PY_EXACT_ARGS_4_r01] = "_INIT_CALL_PY_EXACT_ARGS_4_r01", [_INSERT_NULL] = "_INSERT_NULL", + [_INSERT_NULL_r10] = "_INSERT_NULL_r10", [_IS_NONE] = "_IS_NONE", + [_IS_NONE_r11] = "_IS_NONE_r11", [_IS_OP] = "_IS_OP", + [_IS_OP_r03] = "_IS_OP_r03", + [_IS_OP_r13] = "_IS_OP_r13", + [_IS_OP_r23] = "_IS_OP_r23", [_ITER_CHECK_LIST] = "_ITER_CHECK_LIST", + [_ITER_CHECK_LIST_r02] = "_ITER_CHECK_LIST_r02", + [_ITER_CHECK_LIST_r12] = "_ITER_CHECK_LIST_r12", + [_ITER_CHECK_LIST_r22] = "_ITER_CHECK_LIST_r22", + [_ITER_CHECK_LIST_r33] = "_ITER_CHECK_LIST_r33", [_ITER_CHECK_RANGE] = "_ITER_CHECK_RANGE", + [_ITER_CHECK_RANGE_r02] = "_ITER_CHECK_RANGE_r02", + [_ITER_CHECK_RANGE_r12] = "_ITER_CHECK_RANGE_r12", + [_ITER_CHECK_RANGE_r22] = "_ITER_CHECK_RANGE_r22", + [_ITER_CHECK_RANGE_r33] = "_ITER_CHECK_RANGE_r33", [_ITER_CHECK_TUPLE] = "_ITER_CHECK_TUPLE", + [_ITER_CHECK_TUPLE_r02] = "_ITER_CHECK_TUPLE_r02", + [_ITER_CHECK_TUPLE_r12] = "_ITER_CHECK_TUPLE_r12", + [_ITER_CHECK_TUPLE_r22] = "_ITER_CHECK_TUPLE_r22", + [_ITER_CHECK_TUPLE_r33] = "_ITER_CHECK_TUPLE_r33", + [_ITER_NEXT_INLINE] = "_ITER_NEXT_INLINE", + [_ITER_NEXT_INLINE_r23] = "_ITER_NEXT_INLINE_r23", [_ITER_NEXT_LIST_TIER_TWO] = "_ITER_NEXT_LIST_TIER_TWO", + [_ITER_NEXT_LIST_TIER_TWO_r23] = "_ITER_NEXT_LIST_TIER_TWO_r23", [_ITER_NEXT_RANGE] = "_ITER_NEXT_RANGE", + [_ITER_NEXT_RANGE_r03] = "_ITER_NEXT_RANGE_r03", + [_ITER_NEXT_RANGE_r13] = "_ITER_NEXT_RANGE_r13", + [_ITER_NEXT_RANGE_r23] = "_ITER_NEXT_RANGE_r23", [_ITER_NEXT_TUPLE] = "_ITER_NEXT_TUPLE", + [_ITER_NEXT_TUPLE_r03] = "_ITER_NEXT_TUPLE_r03", + [_ITER_NEXT_TUPLE_r13] = "_ITER_NEXT_TUPLE_r13", + [_ITER_NEXT_TUPLE_r23] = "_ITER_NEXT_TUPLE_r23", [_JUMP_TO_TOP] = "_JUMP_TO_TOP", + [_JUMP_TO_TOP_r00] = "_JUMP_TO_TOP_r00", [_LIST_APPEND] = "_LIST_APPEND", + [_LIST_APPEND_r10] = "_LIST_APPEND_r10", [_LIST_EXTEND] = "_LIST_EXTEND", + [_LIST_EXTEND_r11] = "_LIST_EXTEND_r11", [_LOAD_ATTR] = "_LOAD_ATTR", + [_LOAD_ATTR_r10] = "_LOAD_ATTR_r10", [_LOAD_ATTR_CLASS] = "_LOAD_ATTR_CLASS", + [_LOAD_ATTR_CLASS_r11] = "_LOAD_ATTR_CLASS_r11", + [_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME] = "_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME", + [_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME_r11] = "_LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME_r11", [_LOAD_ATTR_INSTANCE_VALUE] = "_LOAD_ATTR_INSTANCE_VALUE", + [_LOAD_ATTR_INSTANCE_VALUE_r02] = "_LOAD_ATTR_INSTANCE_VALUE_r02", + [_LOAD_ATTR_INSTANCE_VALUE_r12] = "_LOAD_ATTR_INSTANCE_VALUE_r12", + [_LOAD_ATTR_INSTANCE_VALUE_r23] = "_LOAD_ATTR_INSTANCE_VALUE_r23", [_LOAD_ATTR_METHOD_LAZY_DICT] = "_LOAD_ATTR_METHOD_LAZY_DICT", + [_LOAD_ATTR_METHOD_LAZY_DICT_r02] = "_LOAD_ATTR_METHOD_LAZY_DICT_r02", + [_LOAD_ATTR_METHOD_LAZY_DICT_r12] = "_LOAD_ATTR_METHOD_LAZY_DICT_r12", + [_LOAD_ATTR_METHOD_LAZY_DICT_r23] = "_LOAD_ATTR_METHOD_LAZY_DICT_r23", [_LOAD_ATTR_METHOD_NO_DICT] = "_LOAD_ATTR_METHOD_NO_DICT", + [_LOAD_ATTR_METHOD_NO_DICT_r02] = "_LOAD_ATTR_METHOD_NO_DICT_r02", + [_LOAD_ATTR_METHOD_NO_DICT_r12] = "_LOAD_ATTR_METHOD_NO_DICT_r12", + [_LOAD_ATTR_METHOD_NO_DICT_r23] = "_LOAD_ATTR_METHOD_NO_DICT_r23", [_LOAD_ATTR_METHOD_WITH_VALUES] = "_LOAD_ATTR_METHOD_WITH_VALUES", + [_LOAD_ATTR_METHOD_WITH_VALUES_r02] = "_LOAD_ATTR_METHOD_WITH_VALUES_r02", + [_LOAD_ATTR_METHOD_WITH_VALUES_r12] = "_LOAD_ATTR_METHOD_WITH_VALUES_r12", + [_LOAD_ATTR_METHOD_WITH_VALUES_r23] = "_LOAD_ATTR_METHOD_WITH_VALUES_r23", [_LOAD_ATTR_MODULE] = "_LOAD_ATTR_MODULE", + [_LOAD_ATTR_MODULE_r12] = "_LOAD_ATTR_MODULE_r12", [_LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = "_LOAD_ATTR_NONDESCRIPTOR_NO_DICT", + [_LOAD_ATTR_NONDESCRIPTOR_NO_DICT_r11] = "_LOAD_ATTR_NONDESCRIPTOR_NO_DICT_r11", [_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = "_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES", + [_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES_r11] = "_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES_r11", [_LOAD_ATTR_PROPERTY_FRAME] = "_LOAD_ATTR_PROPERTY_FRAME", + [_LOAD_ATTR_PROPERTY_FRAME_r01] = "_LOAD_ATTR_PROPERTY_FRAME_r01", + [_LOAD_ATTR_PROPERTY_FRAME_r11] = "_LOAD_ATTR_PROPERTY_FRAME_r11", + [_LOAD_ATTR_PROPERTY_FRAME_r22] = "_LOAD_ATTR_PROPERTY_FRAME_r22", + [_LOAD_ATTR_PROPERTY_FRAME_r33] = "_LOAD_ATTR_PROPERTY_FRAME_r33", [_LOAD_ATTR_SLOT] = "_LOAD_ATTR_SLOT", + [_LOAD_ATTR_SLOT_r02] = "_LOAD_ATTR_SLOT_r02", + [_LOAD_ATTR_SLOT_r12] = "_LOAD_ATTR_SLOT_r12", + [_LOAD_ATTR_SLOT_r23] = "_LOAD_ATTR_SLOT_r23", [_LOAD_ATTR_WITH_HINT] = "_LOAD_ATTR_WITH_HINT", + [_LOAD_ATTR_WITH_HINT_r12] = "_LOAD_ATTR_WITH_HINT_r12", [_LOAD_BUILD_CLASS] = "_LOAD_BUILD_CLASS", + [_LOAD_BUILD_CLASS_r01] = "_LOAD_BUILD_CLASS_r01", [_LOAD_COMMON_CONSTANT] = "_LOAD_COMMON_CONSTANT", - [_LOAD_CONST_IMMORTAL] = "_LOAD_CONST_IMMORTAL", + [_LOAD_COMMON_CONSTANT_r01] = "_LOAD_COMMON_CONSTANT_r01", + [_LOAD_COMMON_CONSTANT_r12] = "_LOAD_COMMON_CONSTANT_r12", + [_LOAD_COMMON_CONSTANT_r23] = "_LOAD_COMMON_CONSTANT_r23", + [_LOAD_CONST] = "_LOAD_CONST", + [_LOAD_CONST_r01] = "_LOAD_CONST_r01", + [_LOAD_CONST_r12] = "_LOAD_CONST_r12", + [_LOAD_CONST_r23] = "_LOAD_CONST_r23", [_LOAD_CONST_INLINE] = "_LOAD_CONST_INLINE", + [_LOAD_CONST_INLINE_r01] = "_LOAD_CONST_INLINE_r01", + [_LOAD_CONST_INLINE_r12] = "_LOAD_CONST_INLINE_r12", + [_LOAD_CONST_INLINE_r23] = "_LOAD_CONST_INLINE_r23", [_LOAD_CONST_INLINE_BORROW] = "_LOAD_CONST_INLINE_BORROW", - [_LOAD_CONST_MORTAL] = "_LOAD_CONST_MORTAL", + [_LOAD_CONST_INLINE_BORROW_r01] = "_LOAD_CONST_INLINE_BORROW_r01", + [_LOAD_CONST_INLINE_BORROW_r12] = "_LOAD_CONST_INLINE_BORROW_r12", + [_LOAD_CONST_INLINE_BORROW_r23] = "_LOAD_CONST_INLINE_BORROW_r23", [_LOAD_DEREF] = "_LOAD_DEREF", + [_LOAD_DEREF_r01] = "_LOAD_DEREF_r01", [_LOAD_FAST] = "_LOAD_FAST", + [_LOAD_FAST_r01] = "_LOAD_FAST_r01", + [_LOAD_FAST_r12] = "_LOAD_FAST_r12", + [_LOAD_FAST_r23] = "_LOAD_FAST_r23", [_LOAD_FAST_0] = "_LOAD_FAST_0", + [_LOAD_FAST_0_r01] = "_LOAD_FAST_0_r01", + [_LOAD_FAST_0_r12] = "_LOAD_FAST_0_r12", + [_LOAD_FAST_0_r23] = "_LOAD_FAST_0_r23", [_LOAD_FAST_1] = "_LOAD_FAST_1", + [_LOAD_FAST_1_r01] = "_LOAD_FAST_1_r01", + [_LOAD_FAST_1_r12] = "_LOAD_FAST_1_r12", + [_LOAD_FAST_1_r23] = "_LOAD_FAST_1_r23", [_LOAD_FAST_2] = "_LOAD_FAST_2", + [_LOAD_FAST_2_r01] = "_LOAD_FAST_2_r01", + [_LOAD_FAST_2_r12] = "_LOAD_FAST_2_r12", + [_LOAD_FAST_2_r23] = "_LOAD_FAST_2_r23", [_LOAD_FAST_3] = "_LOAD_FAST_3", + [_LOAD_FAST_3_r01] = "_LOAD_FAST_3_r01", + [_LOAD_FAST_3_r12] = "_LOAD_FAST_3_r12", + [_LOAD_FAST_3_r23] = "_LOAD_FAST_3_r23", [_LOAD_FAST_4] = "_LOAD_FAST_4", + [_LOAD_FAST_4_r01] = "_LOAD_FAST_4_r01", + [_LOAD_FAST_4_r12] = "_LOAD_FAST_4_r12", + [_LOAD_FAST_4_r23] = "_LOAD_FAST_4_r23", [_LOAD_FAST_5] = "_LOAD_FAST_5", + [_LOAD_FAST_5_r01] = "_LOAD_FAST_5_r01", + [_LOAD_FAST_5_r12] = "_LOAD_FAST_5_r12", + [_LOAD_FAST_5_r23] = "_LOAD_FAST_5_r23", [_LOAD_FAST_6] = "_LOAD_FAST_6", + [_LOAD_FAST_6_r01] = "_LOAD_FAST_6_r01", + [_LOAD_FAST_6_r12] = "_LOAD_FAST_6_r12", + [_LOAD_FAST_6_r23] = "_LOAD_FAST_6_r23", [_LOAD_FAST_7] = "_LOAD_FAST_7", + [_LOAD_FAST_7_r01] = "_LOAD_FAST_7_r01", + [_LOAD_FAST_7_r12] = "_LOAD_FAST_7_r12", + [_LOAD_FAST_7_r23] = "_LOAD_FAST_7_r23", [_LOAD_FAST_AND_CLEAR] = "_LOAD_FAST_AND_CLEAR", + [_LOAD_FAST_AND_CLEAR_r01] = "_LOAD_FAST_AND_CLEAR_r01", + [_LOAD_FAST_AND_CLEAR_r12] = "_LOAD_FAST_AND_CLEAR_r12", + [_LOAD_FAST_AND_CLEAR_r23] = "_LOAD_FAST_AND_CLEAR_r23", [_LOAD_FAST_BORROW] = "_LOAD_FAST_BORROW", + [_LOAD_FAST_BORROW_r01] = "_LOAD_FAST_BORROW_r01", + [_LOAD_FAST_BORROW_r12] = "_LOAD_FAST_BORROW_r12", + [_LOAD_FAST_BORROW_r23] = "_LOAD_FAST_BORROW_r23", [_LOAD_FAST_BORROW_0] = "_LOAD_FAST_BORROW_0", + [_LOAD_FAST_BORROW_0_r01] = "_LOAD_FAST_BORROW_0_r01", + [_LOAD_FAST_BORROW_0_r12] = "_LOAD_FAST_BORROW_0_r12", + [_LOAD_FAST_BORROW_0_r23] = "_LOAD_FAST_BORROW_0_r23", [_LOAD_FAST_BORROW_1] = "_LOAD_FAST_BORROW_1", + [_LOAD_FAST_BORROW_1_r01] = "_LOAD_FAST_BORROW_1_r01", + [_LOAD_FAST_BORROW_1_r12] = "_LOAD_FAST_BORROW_1_r12", + [_LOAD_FAST_BORROW_1_r23] = "_LOAD_FAST_BORROW_1_r23", [_LOAD_FAST_BORROW_2] = "_LOAD_FAST_BORROW_2", + [_LOAD_FAST_BORROW_2_r01] = "_LOAD_FAST_BORROW_2_r01", + [_LOAD_FAST_BORROW_2_r12] = "_LOAD_FAST_BORROW_2_r12", + [_LOAD_FAST_BORROW_2_r23] = "_LOAD_FAST_BORROW_2_r23", [_LOAD_FAST_BORROW_3] = "_LOAD_FAST_BORROW_3", + [_LOAD_FAST_BORROW_3_r01] = "_LOAD_FAST_BORROW_3_r01", + [_LOAD_FAST_BORROW_3_r12] = "_LOAD_FAST_BORROW_3_r12", + [_LOAD_FAST_BORROW_3_r23] = "_LOAD_FAST_BORROW_3_r23", [_LOAD_FAST_BORROW_4] = "_LOAD_FAST_BORROW_4", + [_LOAD_FAST_BORROW_4_r01] = "_LOAD_FAST_BORROW_4_r01", + [_LOAD_FAST_BORROW_4_r12] = "_LOAD_FAST_BORROW_4_r12", + [_LOAD_FAST_BORROW_4_r23] = "_LOAD_FAST_BORROW_4_r23", [_LOAD_FAST_BORROW_5] = "_LOAD_FAST_BORROW_5", + [_LOAD_FAST_BORROW_5_r01] = "_LOAD_FAST_BORROW_5_r01", + [_LOAD_FAST_BORROW_5_r12] = "_LOAD_FAST_BORROW_5_r12", + [_LOAD_FAST_BORROW_5_r23] = "_LOAD_FAST_BORROW_5_r23", [_LOAD_FAST_BORROW_6] = "_LOAD_FAST_BORROW_6", + [_LOAD_FAST_BORROW_6_r01] = "_LOAD_FAST_BORROW_6_r01", + [_LOAD_FAST_BORROW_6_r12] = "_LOAD_FAST_BORROW_6_r12", + [_LOAD_FAST_BORROW_6_r23] = "_LOAD_FAST_BORROW_6_r23", [_LOAD_FAST_BORROW_7] = "_LOAD_FAST_BORROW_7", - [_LOAD_FAST_BORROW_LOAD_FAST_BORROW] = "_LOAD_FAST_BORROW_LOAD_FAST_BORROW", + [_LOAD_FAST_BORROW_7_r01] = "_LOAD_FAST_BORROW_7_r01", + [_LOAD_FAST_BORROW_7_r12] = "_LOAD_FAST_BORROW_7_r12", + [_LOAD_FAST_BORROW_7_r23] = "_LOAD_FAST_BORROW_7_r23", [_LOAD_FAST_CHECK] = "_LOAD_FAST_CHECK", - [_LOAD_FAST_LOAD_FAST] = "_LOAD_FAST_LOAD_FAST", + [_LOAD_FAST_CHECK_r01] = "_LOAD_FAST_CHECK_r01", + [_LOAD_FAST_CHECK_r12] = "_LOAD_FAST_CHECK_r12", + [_LOAD_FAST_CHECK_r23] = "_LOAD_FAST_CHECK_r23", [_LOAD_FROM_DICT_OR_DEREF] = "_LOAD_FROM_DICT_OR_DEREF", + [_LOAD_FROM_DICT_OR_DEREF_r11] = "_LOAD_FROM_DICT_OR_DEREF_r11", + [_LOAD_FROM_DICT_OR_GLOBALS] = "_LOAD_FROM_DICT_OR_GLOBALS", + [_LOAD_FROM_DICT_OR_GLOBALS_r11] = "_LOAD_FROM_DICT_OR_GLOBALS_r11", [_LOAD_GLOBAL] = "_LOAD_GLOBAL", + [_LOAD_GLOBAL_r00] = "_LOAD_GLOBAL_r00", [_LOAD_GLOBAL_BUILTINS] = "_LOAD_GLOBAL_BUILTINS", + [_LOAD_GLOBAL_BUILTINS_r01] = "_LOAD_GLOBAL_BUILTINS_r01", [_LOAD_GLOBAL_MODULE] = "_LOAD_GLOBAL_MODULE", + [_LOAD_GLOBAL_MODULE_r01] = "_LOAD_GLOBAL_MODULE_r01", [_LOAD_LOCALS] = "_LOAD_LOCALS", + [_LOAD_LOCALS_r01] = "_LOAD_LOCALS_r01", + [_LOAD_LOCALS_r12] = "_LOAD_LOCALS_r12", + [_LOAD_LOCALS_r23] = "_LOAD_LOCALS_r23", [_LOAD_NAME] = "_LOAD_NAME", + [_LOAD_NAME_r01] = "_LOAD_NAME_r01", [_LOAD_SMALL_INT] = "_LOAD_SMALL_INT", + [_LOAD_SMALL_INT_r01] = "_LOAD_SMALL_INT_r01", + [_LOAD_SMALL_INT_r12] = "_LOAD_SMALL_INT_r12", + [_LOAD_SMALL_INT_r23] = "_LOAD_SMALL_INT_r23", [_LOAD_SMALL_INT_0] = "_LOAD_SMALL_INT_0", + [_LOAD_SMALL_INT_0_r01] = "_LOAD_SMALL_INT_0_r01", + [_LOAD_SMALL_INT_0_r12] = "_LOAD_SMALL_INT_0_r12", + [_LOAD_SMALL_INT_0_r23] = "_LOAD_SMALL_INT_0_r23", [_LOAD_SMALL_INT_1] = "_LOAD_SMALL_INT_1", + [_LOAD_SMALL_INT_1_r01] = "_LOAD_SMALL_INT_1_r01", + [_LOAD_SMALL_INT_1_r12] = "_LOAD_SMALL_INT_1_r12", + [_LOAD_SMALL_INT_1_r23] = "_LOAD_SMALL_INT_1_r23", [_LOAD_SMALL_INT_2] = "_LOAD_SMALL_INT_2", + [_LOAD_SMALL_INT_2_r01] = "_LOAD_SMALL_INT_2_r01", + [_LOAD_SMALL_INT_2_r12] = "_LOAD_SMALL_INT_2_r12", + [_LOAD_SMALL_INT_2_r23] = "_LOAD_SMALL_INT_2_r23", [_LOAD_SMALL_INT_3] = "_LOAD_SMALL_INT_3", + [_LOAD_SMALL_INT_3_r01] = "_LOAD_SMALL_INT_3_r01", + [_LOAD_SMALL_INT_3_r12] = "_LOAD_SMALL_INT_3_r12", + [_LOAD_SMALL_INT_3_r23] = "_LOAD_SMALL_INT_3_r23", [_LOAD_SPECIAL] = "_LOAD_SPECIAL", + [_LOAD_SPECIAL_r00] = "_LOAD_SPECIAL_r00", [_LOAD_SUPER_ATTR_ATTR] = "_LOAD_SUPER_ATTR_ATTR", + [_LOAD_SUPER_ATTR_ATTR_r31] = "_LOAD_SUPER_ATTR_ATTR_r31", [_LOAD_SUPER_ATTR_METHOD] = "_LOAD_SUPER_ATTR_METHOD", + [_LOAD_SUPER_ATTR_METHOD_r32] = "_LOAD_SUPER_ATTR_METHOD_r32", + [_LOCK_OBJECT] = "_LOCK_OBJECT", + [_LOCK_OBJECT_r01] = "_LOCK_OBJECT_r01", + [_LOCK_OBJECT_r11] = "_LOCK_OBJECT_r11", + [_LOCK_OBJECT_r22] = "_LOCK_OBJECT_r22", + [_LOCK_OBJECT_r33] = "_LOCK_OBJECT_r33", [_MAKE_CALLARGS_A_TUPLE] = "_MAKE_CALLARGS_A_TUPLE", + [_MAKE_CALLARGS_A_TUPLE_r33] = "_MAKE_CALLARGS_A_TUPLE_r33", [_MAKE_CELL] = "_MAKE_CELL", + [_MAKE_CELL_r00] = "_MAKE_CELL_r00", [_MAKE_FUNCTION] = "_MAKE_FUNCTION", + [_MAKE_FUNCTION_r12] = "_MAKE_FUNCTION_r12", + [_MAKE_HEAP_SAFE] = "_MAKE_HEAP_SAFE", + [_MAKE_HEAP_SAFE_r01] = "_MAKE_HEAP_SAFE_r01", + [_MAKE_HEAP_SAFE_r11] = "_MAKE_HEAP_SAFE_r11", + [_MAKE_HEAP_SAFE_r22] = "_MAKE_HEAP_SAFE_r22", + [_MAKE_HEAP_SAFE_r33] = "_MAKE_HEAP_SAFE_r33", [_MAKE_WARM] = "_MAKE_WARM", + [_MAKE_WARM_r00] = "_MAKE_WARM_r00", + [_MAKE_WARM_r11] = "_MAKE_WARM_r11", + [_MAKE_WARM_r22] = "_MAKE_WARM_r22", + [_MAKE_WARM_r33] = "_MAKE_WARM_r33", [_MAP_ADD] = "_MAP_ADD", + [_MAP_ADD_r20] = "_MAP_ADD_r20", [_MATCH_CLASS] = "_MATCH_CLASS", + [_MATCH_CLASS_r33] = "_MATCH_CLASS_r33", [_MATCH_KEYS] = "_MATCH_KEYS", + [_MATCH_KEYS_r23] = "_MATCH_KEYS_r23", [_MATCH_MAPPING] = "_MATCH_MAPPING", + [_MATCH_MAPPING_r02] = "_MATCH_MAPPING_r02", + [_MATCH_MAPPING_r12] = "_MATCH_MAPPING_r12", + [_MATCH_MAPPING_r23] = "_MATCH_MAPPING_r23", [_MATCH_SEQUENCE] = "_MATCH_SEQUENCE", + [_MATCH_SEQUENCE_r02] = "_MATCH_SEQUENCE_r02", + [_MATCH_SEQUENCE_r12] = "_MATCH_SEQUENCE_r12", + [_MATCH_SEQUENCE_r23] = "_MATCH_SEQUENCE_r23", [_MAYBE_EXPAND_METHOD] = "_MAYBE_EXPAND_METHOD", + [_MAYBE_EXPAND_METHOD_r00] = "_MAYBE_EXPAND_METHOD_r00", [_MAYBE_EXPAND_METHOD_KW] = "_MAYBE_EXPAND_METHOD_KW", + [_MAYBE_EXPAND_METHOD_KW_r11] = "_MAYBE_EXPAND_METHOD_KW_r11", [_NOP] = "_NOP", + [_NOP_r00] = "_NOP_r00", + [_NOP_r11] = "_NOP_r11", + [_NOP_r22] = "_NOP_r22", + [_NOP_r33] = "_NOP_r33", [_POP_EXCEPT] = "_POP_EXCEPT", + [_POP_EXCEPT_r10] = "_POP_EXCEPT_r10", + [_POP_ITER] = "_POP_ITER", + [_POP_ITER_r20] = "_POP_ITER_r20", [_POP_TOP] = "_POP_TOP", - [_POP_TOP_LOAD_CONST_INLINE] = "_POP_TOP_LOAD_CONST_INLINE", - [_POP_TOP_LOAD_CONST_INLINE_BORROW] = "_POP_TOP_LOAD_CONST_INLINE_BORROW", - [_POP_TWO_LOAD_CONST_INLINE_BORROW] = "_POP_TWO_LOAD_CONST_INLINE_BORROW", + [_POP_TOP_r10] = "_POP_TOP_r10", + [_POP_TOP_FLOAT] = "_POP_TOP_FLOAT", + [_POP_TOP_FLOAT_r00] = "_POP_TOP_FLOAT_r00", + [_POP_TOP_FLOAT_r10] = "_POP_TOP_FLOAT_r10", + [_POP_TOP_FLOAT_r21] = "_POP_TOP_FLOAT_r21", + [_POP_TOP_FLOAT_r32] = "_POP_TOP_FLOAT_r32", + [_POP_TOP_INT] = "_POP_TOP_INT", + [_POP_TOP_INT_r00] = "_POP_TOP_INT_r00", + [_POP_TOP_INT_r10] = "_POP_TOP_INT_r10", + [_POP_TOP_INT_r21] = "_POP_TOP_INT_r21", + [_POP_TOP_INT_r32] = "_POP_TOP_INT_r32", + [_POP_TOP_NOP] = "_POP_TOP_NOP", + [_POP_TOP_NOP_r00] = "_POP_TOP_NOP_r00", + [_POP_TOP_NOP_r10] = "_POP_TOP_NOP_r10", + [_POP_TOP_NOP_r21] = "_POP_TOP_NOP_r21", + [_POP_TOP_NOP_r32] = "_POP_TOP_NOP_r32", + [_POP_TOP_OPARG] = "_POP_TOP_OPARG", + [_POP_TOP_OPARG_r00] = "_POP_TOP_OPARG_r00", + [_POP_TOP_UNICODE] = "_POP_TOP_UNICODE", + [_POP_TOP_UNICODE_r00] = "_POP_TOP_UNICODE_r00", + [_POP_TOP_UNICODE_r10] = "_POP_TOP_UNICODE_r10", + [_POP_TOP_UNICODE_r21] = "_POP_TOP_UNICODE_r21", + [_POP_TOP_UNICODE_r32] = "_POP_TOP_UNICODE_r32", [_PUSH_EXC_INFO] = "_PUSH_EXC_INFO", + [_PUSH_EXC_INFO_r02] = "_PUSH_EXC_INFO_r02", + [_PUSH_EXC_INFO_r12] = "_PUSH_EXC_INFO_r12", + [_PUSH_EXC_INFO_r23] = "_PUSH_EXC_INFO_r23", [_PUSH_FRAME] = "_PUSH_FRAME", + [_PUSH_FRAME_r10] = "_PUSH_FRAME_r10", [_PUSH_NULL] = "_PUSH_NULL", + [_PUSH_NULL_r01] = "_PUSH_NULL_r01", + [_PUSH_NULL_r12] = "_PUSH_NULL_r12", + [_PUSH_NULL_r23] = "_PUSH_NULL_r23", [_PUSH_NULL_CONDITIONAL] = "_PUSH_NULL_CONDITIONAL", + [_PUSH_NULL_CONDITIONAL_r00] = "_PUSH_NULL_CONDITIONAL_r00", + [_PUSH_TAGGED_ZERO] = "_PUSH_TAGGED_ZERO", + [_PUSH_TAGGED_ZERO_r01] = "_PUSH_TAGGED_ZERO_r01", + [_PUSH_TAGGED_ZERO_r12] = "_PUSH_TAGGED_ZERO_r12", + [_PUSH_TAGGED_ZERO_r23] = "_PUSH_TAGGED_ZERO_r23", + [_PY_FRAME_EX] = "_PY_FRAME_EX", + [_PY_FRAME_EX_r31] = "_PY_FRAME_EX_r31", [_PY_FRAME_GENERAL] = "_PY_FRAME_GENERAL", + [_PY_FRAME_GENERAL_r01] = "_PY_FRAME_GENERAL_r01", [_PY_FRAME_KW] = "_PY_FRAME_KW", + [_PY_FRAME_KW_r11] = "_PY_FRAME_KW_r11", + [_RECORD_3OS_GEN_FUNC] = "_RECORD_3OS_GEN_FUNC", + [_RECORD_4OS] = "_RECORD_4OS", + [_RECORD_BOUND_METHOD] = "_RECORD_BOUND_METHOD", + [_RECORD_CALLABLE] = "_RECORD_CALLABLE", + [_RECORD_CALLABLE_KW] = "_RECORD_CALLABLE_KW", + [_RECORD_CODE] = "_RECORD_CODE", + [_RECORD_NOS] = "_RECORD_NOS", + [_RECORD_NOS_GEN_FUNC] = "_RECORD_NOS_GEN_FUNC", + [_RECORD_NOS_TYPE] = "_RECORD_NOS_TYPE", + [_RECORD_TOS] = "_RECORD_TOS", + [_RECORD_TOS_TYPE] = "_RECORD_TOS_TYPE", [_REPLACE_WITH_TRUE] = "_REPLACE_WITH_TRUE", + [_REPLACE_WITH_TRUE_r02] = "_REPLACE_WITH_TRUE_r02", + [_REPLACE_WITH_TRUE_r12] = "_REPLACE_WITH_TRUE_r12", + [_REPLACE_WITH_TRUE_r23] = "_REPLACE_WITH_TRUE_r23", [_RESUME_CHECK] = "_RESUME_CHECK", + [_RESUME_CHECK_r00] = "_RESUME_CHECK_r00", + [_RESUME_CHECK_r11] = "_RESUME_CHECK_r11", + [_RESUME_CHECK_r22] = "_RESUME_CHECK_r22", + [_RESUME_CHECK_r33] = "_RESUME_CHECK_r33", [_RETURN_GENERATOR] = "_RETURN_GENERATOR", + [_RETURN_GENERATOR_r01] = "_RETURN_GENERATOR_r01", [_RETURN_VALUE] = "_RETURN_VALUE", + [_RETURN_VALUE_r11] = "_RETURN_VALUE_r11", + [_RROT_3] = "_RROT_3", + [_RROT_3_r03] = "_RROT_3_r03", + [_RROT_3_r13] = "_RROT_3_r13", + [_RROT_3_r23] = "_RROT_3_r23", + [_RROT_3_r33] = "_RROT_3_r33", [_SAVE_RETURN_OFFSET] = "_SAVE_RETURN_OFFSET", + [_SAVE_RETURN_OFFSET_r00] = "_SAVE_RETURN_OFFSET_r00", + [_SAVE_RETURN_OFFSET_r11] = "_SAVE_RETURN_OFFSET_r11", + [_SAVE_RETURN_OFFSET_r22] = "_SAVE_RETURN_OFFSET_r22", + [_SAVE_RETURN_OFFSET_r33] = "_SAVE_RETURN_OFFSET_r33", + [_SEND_ASYNC_GEN_TIER_TWO] = "_SEND_ASYNC_GEN_TIER_TWO", + [_SEND_ASYNC_GEN_TIER_TWO_r33] = "_SEND_ASYNC_GEN_TIER_TWO_r33", [_SEND_GEN_FRAME] = "_SEND_GEN_FRAME", + [_SEND_GEN_FRAME_r33] = "_SEND_GEN_FRAME_r33", + [_SEND_VIRTUAL_TIER_TWO] = "_SEND_VIRTUAL_TIER_TWO", + [_SEND_VIRTUAL_TIER_TWO_r03] = "_SEND_VIRTUAL_TIER_TWO_r03", + [_SEND_VIRTUAL_TIER_TWO_r13] = "_SEND_VIRTUAL_TIER_TWO_r13", + [_SEND_VIRTUAL_TIER_TWO_r23] = "_SEND_VIRTUAL_TIER_TWO_r23", + [_SEND_VIRTUAL_TIER_TWO_r33] = "_SEND_VIRTUAL_TIER_TWO_r33", [_SETUP_ANNOTATIONS] = "_SETUP_ANNOTATIONS", + [_SETUP_ANNOTATIONS_r00] = "_SETUP_ANNOTATIONS_r00", [_SET_ADD] = "_SET_ADD", + [_SET_ADD_r10] = "_SET_ADD_r10", [_SET_FUNCTION_ATTRIBUTE] = "_SET_FUNCTION_ATTRIBUTE", + [_SET_FUNCTION_ATTRIBUTE_r01] = "_SET_FUNCTION_ATTRIBUTE_r01", + [_SET_FUNCTION_ATTRIBUTE_r11] = "_SET_FUNCTION_ATTRIBUTE_r11", + [_SET_FUNCTION_ATTRIBUTE_r21] = "_SET_FUNCTION_ATTRIBUTE_r21", + [_SET_FUNCTION_ATTRIBUTE_r32] = "_SET_FUNCTION_ATTRIBUTE_r32", [_SET_IP] = "_SET_IP", + [_SET_IP_r00] = "_SET_IP_r00", + [_SET_IP_r11] = "_SET_IP_r11", + [_SET_IP_r22] = "_SET_IP_r22", + [_SET_IP_r33] = "_SET_IP_r33", [_SET_UPDATE] = "_SET_UPDATE", + [_SET_UPDATE_r11] = "_SET_UPDATE_r11", + [_SPILL_OR_RELOAD] = "_SPILL_OR_RELOAD", + [_SPILL_OR_RELOAD_r01] = "_SPILL_OR_RELOAD_r01", + [_SPILL_OR_RELOAD_r02] = "_SPILL_OR_RELOAD_r02", + [_SPILL_OR_RELOAD_r03] = "_SPILL_OR_RELOAD_r03", + [_SPILL_OR_RELOAD_r10] = "_SPILL_OR_RELOAD_r10", + [_SPILL_OR_RELOAD_r12] = "_SPILL_OR_RELOAD_r12", + [_SPILL_OR_RELOAD_r13] = "_SPILL_OR_RELOAD_r13", + [_SPILL_OR_RELOAD_r20] = "_SPILL_OR_RELOAD_r20", + [_SPILL_OR_RELOAD_r21] = "_SPILL_OR_RELOAD_r21", + [_SPILL_OR_RELOAD_r23] = "_SPILL_OR_RELOAD_r23", + [_SPILL_OR_RELOAD_r30] = "_SPILL_OR_RELOAD_r30", + [_SPILL_OR_RELOAD_r31] = "_SPILL_OR_RELOAD_r31", + [_SPILL_OR_RELOAD_r32] = "_SPILL_OR_RELOAD_r32", [_START_EXECUTOR] = "_START_EXECUTOR", + [_START_EXECUTOR_r00] = "_START_EXECUTOR_r00", [_STORE_ATTR] = "_STORE_ATTR", + [_STORE_ATTR_r20] = "_STORE_ATTR_r20", [_STORE_ATTR_INSTANCE_VALUE] = "_STORE_ATTR_INSTANCE_VALUE", + [_STORE_ATTR_INSTANCE_VALUE_r21] = "_STORE_ATTR_INSTANCE_VALUE_r21", [_STORE_ATTR_SLOT] = "_STORE_ATTR_SLOT", + [_STORE_ATTR_SLOT_r21] = "_STORE_ATTR_SLOT_r21", [_STORE_ATTR_WITH_HINT] = "_STORE_ATTR_WITH_HINT", + [_STORE_ATTR_WITH_HINT_r21] = "_STORE_ATTR_WITH_HINT_r21", [_STORE_DEREF] = "_STORE_DEREF", - [_STORE_FAST] = "_STORE_FAST", - [_STORE_FAST_0] = "_STORE_FAST_0", - [_STORE_FAST_1] = "_STORE_FAST_1", - [_STORE_FAST_2] = "_STORE_FAST_2", - [_STORE_FAST_3] = "_STORE_FAST_3", - [_STORE_FAST_4] = "_STORE_FAST_4", - [_STORE_FAST_5] = "_STORE_FAST_5", - [_STORE_FAST_6] = "_STORE_FAST_6", - [_STORE_FAST_7] = "_STORE_FAST_7", - [_STORE_FAST_LOAD_FAST] = "_STORE_FAST_LOAD_FAST", - [_STORE_FAST_STORE_FAST] = "_STORE_FAST_STORE_FAST", + [_STORE_DEREF_r10] = "_STORE_DEREF_r10", [_STORE_GLOBAL] = "_STORE_GLOBAL", + [_STORE_GLOBAL_r10] = "_STORE_GLOBAL_r10", [_STORE_NAME] = "_STORE_NAME", + [_STORE_NAME_r10] = "_STORE_NAME_r10", [_STORE_SLICE] = "_STORE_SLICE", + [_STORE_SLICE_r30] = "_STORE_SLICE_r30", [_STORE_SUBSCR] = "_STORE_SUBSCR", + [_STORE_SUBSCR_r30] = "_STORE_SUBSCR_r30", [_STORE_SUBSCR_DICT] = "_STORE_SUBSCR_DICT", + [_STORE_SUBSCR_DICT_r31] = "_STORE_SUBSCR_DICT_r31", + [_STORE_SUBSCR_DICT_KNOWN_HASH] = "_STORE_SUBSCR_DICT_KNOWN_HASH", + [_STORE_SUBSCR_DICT_KNOWN_HASH_r31] = "_STORE_SUBSCR_DICT_KNOWN_HASH_r31", [_STORE_SUBSCR_LIST_INT] = "_STORE_SUBSCR_LIST_INT", + [_STORE_SUBSCR_LIST_INT_r32] = "_STORE_SUBSCR_LIST_INT_r32", [_SWAP] = "_SWAP", + [_SWAP_r11] = "_SWAP_r11", + [_SWAP_2] = "_SWAP_2", + [_SWAP_2_r02] = "_SWAP_2_r02", + [_SWAP_2_r12] = "_SWAP_2_r12", + [_SWAP_2_r22] = "_SWAP_2_r22", + [_SWAP_2_r33] = "_SWAP_2_r33", + [_SWAP_3] = "_SWAP_3", + [_SWAP_3_r03] = "_SWAP_3_r03", + [_SWAP_3_r13] = "_SWAP_3_r13", + [_SWAP_3_r23] = "_SWAP_3_r23", + [_SWAP_3_r33] = "_SWAP_3_r33", + [_SWAP_FAST] = "_SWAP_FAST", + [_SWAP_FAST_r01] = "_SWAP_FAST_r01", + [_SWAP_FAST_r11] = "_SWAP_FAST_r11", + [_SWAP_FAST_r22] = "_SWAP_FAST_r22", + [_SWAP_FAST_r33] = "_SWAP_FAST_r33", + [_SWAP_FAST_0] = "_SWAP_FAST_0", + [_SWAP_FAST_0_r01] = "_SWAP_FAST_0_r01", + [_SWAP_FAST_0_r11] = "_SWAP_FAST_0_r11", + [_SWAP_FAST_0_r22] = "_SWAP_FAST_0_r22", + [_SWAP_FAST_0_r33] = "_SWAP_FAST_0_r33", + [_SWAP_FAST_1] = "_SWAP_FAST_1", + [_SWAP_FAST_1_r01] = "_SWAP_FAST_1_r01", + [_SWAP_FAST_1_r11] = "_SWAP_FAST_1_r11", + [_SWAP_FAST_1_r22] = "_SWAP_FAST_1_r22", + [_SWAP_FAST_1_r33] = "_SWAP_FAST_1_r33", + [_SWAP_FAST_2] = "_SWAP_FAST_2", + [_SWAP_FAST_2_r01] = "_SWAP_FAST_2_r01", + [_SWAP_FAST_2_r11] = "_SWAP_FAST_2_r11", + [_SWAP_FAST_2_r22] = "_SWAP_FAST_2_r22", + [_SWAP_FAST_2_r33] = "_SWAP_FAST_2_r33", + [_SWAP_FAST_3] = "_SWAP_FAST_3", + [_SWAP_FAST_3_r01] = "_SWAP_FAST_3_r01", + [_SWAP_FAST_3_r11] = "_SWAP_FAST_3_r11", + [_SWAP_FAST_3_r22] = "_SWAP_FAST_3_r22", + [_SWAP_FAST_3_r33] = "_SWAP_FAST_3_r33", + [_SWAP_FAST_4] = "_SWAP_FAST_4", + [_SWAP_FAST_4_r01] = "_SWAP_FAST_4_r01", + [_SWAP_FAST_4_r11] = "_SWAP_FAST_4_r11", + [_SWAP_FAST_4_r22] = "_SWAP_FAST_4_r22", + [_SWAP_FAST_4_r33] = "_SWAP_FAST_4_r33", + [_SWAP_FAST_5] = "_SWAP_FAST_5", + [_SWAP_FAST_5_r01] = "_SWAP_FAST_5_r01", + [_SWAP_FAST_5_r11] = "_SWAP_FAST_5_r11", + [_SWAP_FAST_5_r22] = "_SWAP_FAST_5_r22", + [_SWAP_FAST_5_r33] = "_SWAP_FAST_5_r33", + [_SWAP_FAST_6] = "_SWAP_FAST_6", + [_SWAP_FAST_6_r01] = "_SWAP_FAST_6_r01", + [_SWAP_FAST_6_r11] = "_SWAP_FAST_6_r11", + [_SWAP_FAST_6_r22] = "_SWAP_FAST_6_r22", + [_SWAP_FAST_6_r33] = "_SWAP_FAST_6_r33", + [_SWAP_FAST_7] = "_SWAP_FAST_7", + [_SWAP_FAST_7_r01] = "_SWAP_FAST_7_r01", + [_SWAP_FAST_7_r11] = "_SWAP_FAST_7_r11", + [_SWAP_FAST_7_r22] = "_SWAP_FAST_7_r22", + [_SWAP_FAST_7_r33] = "_SWAP_FAST_7_r33", [_TIER2_RESUME_CHECK] = "_TIER2_RESUME_CHECK", + [_TIER2_RESUME_CHECK_r00] = "_TIER2_RESUME_CHECK_r00", + [_TIER2_RESUME_CHECK_r11] = "_TIER2_RESUME_CHECK_r11", + [_TIER2_RESUME_CHECK_r22] = "_TIER2_RESUME_CHECK_r22", + [_TIER2_RESUME_CHECK_r33] = "_TIER2_RESUME_CHECK_r33", [_TO_BOOL] = "_TO_BOOL", + [_TO_BOOL_r11] = "_TO_BOOL_r11", [_TO_BOOL_BOOL] = "_TO_BOOL_BOOL", + [_TO_BOOL_BOOL_r01] = "_TO_BOOL_BOOL_r01", + [_TO_BOOL_BOOL_r11] = "_TO_BOOL_BOOL_r11", + [_TO_BOOL_BOOL_r22] = "_TO_BOOL_BOOL_r22", + [_TO_BOOL_BOOL_r33] = "_TO_BOOL_BOOL_r33", [_TO_BOOL_INT] = "_TO_BOOL_INT", + [_TO_BOOL_INT_r02] = "_TO_BOOL_INT_r02", + [_TO_BOOL_INT_r12] = "_TO_BOOL_INT_r12", + [_TO_BOOL_INT_r23] = "_TO_BOOL_INT_r23", [_TO_BOOL_LIST] = "_TO_BOOL_LIST", + [_TO_BOOL_LIST_r02] = "_TO_BOOL_LIST_r02", + [_TO_BOOL_LIST_r12] = "_TO_BOOL_LIST_r12", + [_TO_BOOL_LIST_r23] = "_TO_BOOL_LIST_r23", [_TO_BOOL_NONE] = "_TO_BOOL_NONE", + [_TO_BOOL_NONE_r01] = "_TO_BOOL_NONE_r01", + [_TO_BOOL_NONE_r11] = "_TO_BOOL_NONE_r11", + [_TO_BOOL_NONE_r22] = "_TO_BOOL_NONE_r22", + [_TO_BOOL_NONE_r33] = "_TO_BOOL_NONE_r33", [_TO_BOOL_STR] = "_TO_BOOL_STR", + [_TO_BOOL_STR_r02] = "_TO_BOOL_STR_r02", + [_TO_BOOL_STR_r12] = "_TO_BOOL_STR_r12", + [_TO_BOOL_STR_r23] = "_TO_BOOL_STR_r23", [_UNARY_INVERT] = "_UNARY_INVERT", + [_UNARY_INVERT_r12] = "_UNARY_INVERT_r12", [_UNARY_NEGATIVE] = "_UNARY_NEGATIVE", + [_UNARY_NEGATIVE_r12] = "_UNARY_NEGATIVE_r12", + [_UNARY_NEGATIVE_FLOAT_INPLACE] = "_UNARY_NEGATIVE_FLOAT_INPLACE", + [_UNARY_NEGATIVE_FLOAT_INPLACE_r02] = "_UNARY_NEGATIVE_FLOAT_INPLACE_r02", + [_UNARY_NEGATIVE_FLOAT_INPLACE_r12] = "_UNARY_NEGATIVE_FLOAT_INPLACE_r12", + [_UNARY_NEGATIVE_FLOAT_INPLACE_r23] = "_UNARY_NEGATIVE_FLOAT_INPLACE_r23", [_UNARY_NOT] = "_UNARY_NOT", + [_UNARY_NOT_r01] = "_UNARY_NOT_r01", + [_UNARY_NOT_r11] = "_UNARY_NOT_r11", + [_UNARY_NOT_r22] = "_UNARY_NOT_r22", + [_UNARY_NOT_r33] = "_UNARY_NOT_r33", [_UNPACK_EX] = "_UNPACK_EX", + [_UNPACK_EX_r10] = "_UNPACK_EX_r10", [_UNPACK_SEQUENCE] = "_UNPACK_SEQUENCE", + [_UNPACK_SEQUENCE_r10] = "_UNPACK_SEQUENCE_r10", [_UNPACK_SEQUENCE_LIST] = "_UNPACK_SEQUENCE_LIST", + [_UNPACK_SEQUENCE_LIST_r10] = "_UNPACK_SEQUENCE_LIST_r10", [_UNPACK_SEQUENCE_TUPLE] = "_UNPACK_SEQUENCE_TUPLE", + [_UNPACK_SEQUENCE_TUPLE_r10] = "_UNPACK_SEQUENCE_TUPLE_r10", [_UNPACK_SEQUENCE_TWO_TUPLE] = "_UNPACK_SEQUENCE_TWO_TUPLE", + [_UNPACK_SEQUENCE_TWO_TUPLE_r12] = "_UNPACK_SEQUENCE_TWO_TUPLE_r12", + [_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE] = "_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE", + [_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r03] = "_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r03", + [_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r13] = "_UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE_r13", + [_UNPACK_SEQUENCE_UNIQUE_TUPLE] = "_UNPACK_SEQUENCE_UNIQUE_TUPLE", + [_UNPACK_SEQUENCE_UNIQUE_TUPLE_r10] = "_UNPACK_SEQUENCE_UNIQUE_TUPLE_r10", + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE] = "_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE", + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r02] = "_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r02", + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r12] = "_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r12", + [_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r23] = "_UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE_r23", [_WITH_EXCEPT_START] = "_WITH_EXCEPT_START", + [_WITH_EXCEPT_START_r33] = "_WITH_EXCEPT_START_r33", [_YIELD_VALUE] = "_YIELD_VALUE", + [_YIELD_VALUE_r11] = "_YIELD_VALUE_r11", }; int _PyUop_num_popped(int opcode, int oparg) { @@ -660,13 +6285,7 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _LOAD_FAST_AND_CLEAR: return 0; - case _LOAD_FAST_LOAD_FAST: - return 0; - case _LOAD_FAST_BORROW_LOAD_FAST_BORROW: - return 0; - case _LOAD_CONST_MORTAL: - return 0; - case _LOAD_CONST_IMMORTAL: + case _LOAD_CONST: return 0; case _LOAD_SMALL_INT_0: return 0; @@ -678,38 +6297,48 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _LOAD_SMALL_INT: return 0; - case _STORE_FAST_0: + case _SWAP_FAST_0: return 1; - case _STORE_FAST_1: + case _SWAP_FAST_1: return 1; - case _STORE_FAST_2: + case _SWAP_FAST_2: return 1; - case _STORE_FAST_3: + case _SWAP_FAST_3: return 1; - case _STORE_FAST_4: + case _SWAP_FAST_4: return 1; - case _STORE_FAST_5: + case _SWAP_FAST_5: return 1; - case _STORE_FAST_6: + case _SWAP_FAST_6: return 1; - case _STORE_FAST_7: + case _SWAP_FAST_7: return 1; - case _STORE_FAST: + case _SWAP_FAST: return 1; - case _STORE_FAST_LOAD_FAST: - return 1; - case _STORE_FAST_STORE_FAST: - return 2; case _POP_TOP: return 1; + case _POP_TOP_NOP: + return 1; + case _POP_TOP_INT: + return 1; + case _POP_TOP_FLOAT: + return 1; + case _POP_TOP_UNICODE: + return 1; + case _POP_TOP_OPARG: + return oparg; case _PUSH_NULL: return 0; case _END_FOR: return 1; - case _END_SEND: + case _POP_ITER: return 2; + case _END_SEND: + return 3; case _UNARY_NEGATIVE: return 1; + case _UNARY_NEGATIVE_FLOAT_INPLACE: + return 1; case _UNARY_NOT: return 1; case _TO_BOOL: @@ -728,6 +6357,8 @@ int _PyUop_num_popped(int opcode, int oparg) return 1; case _TO_BOOL_NONE: return 1; + case _GUARD_NOS_COMPACT_ASCII: + return 0; case _GUARD_NOS_UNICODE: return 0; case _GUARD_TOS_UNICODE: @@ -742,12 +6373,28 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _GUARD_TOS_INT: return 0; + case _GUARD_NOS_OVERFLOWED: + return 0; + case _GUARD_TOS_OVERFLOWED: + return 0; case _BINARY_OP_MULTIPLY_INT: return 2; case _BINARY_OP_ADD_INT: return 2; case _BINARY_OP_SUBTRACT_INT: return 2; + case _BINARY_OP_ADD_INT_INPLACE: + return 2; + case _BINARY_OP_SUBTRACT_INT_INPLACE: + return 2; + case _BINARY_OP_MULTIPLY_INT_INPLACE: + return 2; + case _BINARY_OP_ADD_INT_INPLACE_RIGHT: + return 2; + case _BINARY_OP_SUBTRACT_INT_INPLACE_RIGHT: + return 2; + case _BINARY_OP_MULTIPLY_INT_INPLACE_RIGHT: + return 2; case _GUARD_NOS_FLOAT: return 0; case _GUARD_TOS_FLOAT: @@ -758,10 +6405,32 @@ int _PyUop_num_popped(int opcode, int oparg) return 2; case _BINARY_OP_SUBTRACT_FLOAT: return 2; + case _BINARY_OP_ADD_FLOAT_INPLACE: + return 2; + case _BINARY_OP_SUBTRACT_FLOAT_INPLACE: + return 2; + case _BINARY_OP_MULTIPLY_FLOAT_INPLACE: + return 2; + case _BINARY_OP_ADD_FLOAT_INPLACE_RIGHT: + return 2; + case _BINARY_OP_MULTIPLY_FLOAT_INPLACE_RIGHT: + return 2; + case _BINARY_OP_SUBTRACT_FLOAT_INPLACE_RIGHT: + return 2; + case _BINARY_OP_TRUEDIV_FLOAT: + return 2; + case _BINARY_OP_TRUEDIV_FLOAT_INPLACE: + return 2; + case _BINARY_OP_TRUEDIV_FLOAT_INPLACE_RIGHT: + return 2; case _BINARY_OP_ADD_UNICODE: return 2; case _BINARY_OP_INPLACE_ADD_UNICODE: return 2; + case _GUARD_BINARY_OP_EXTEND_LHS: + return 0; + case _GUARD_BINARY_OP_EXTEND_RHS: + return 0; case _GUARD_BINARY_OP_EXTEND: return 0; case _BINARY_OP_EXTEND: @@ -776,16 +6445,28 @@ int _PyUop_num_popped(int opcode, int oparg) return 2; case _BINARY_OP_SUBSCR_STR_INT: return 2; + case _BINARY_OP_SUBSCR_USTR_INT: + return 2; case _GUARD_NOS_TUPLE: return 0; case _GUARD_TOS_TUPLE: return 0; + case _GUARD_BINARY_OP_SUBSCR_TUPLE_INT_BOUNDS: + return 0; case _BINARY_OP_SUBSCR_TUPLE_INT: return 2; - case _GUARD_NOS_DICT: + case _GUARD_NOS_DICT_SUBSCRIPT: + return 0; + case _GUARD_NOS_DICT_STORE_SUBSCRIPT: + return 0; + case _GUARD_TOS_ANY_DICT: return 0; case _GUARD_TOS_DICT: return 0; + case _GUARD_TOS_FROZENDICT: + return 0; + case _BINARY_OP_SUBSCR_DICT_KNOWN_HASH: + return 2; case _BINARY_OP_SUBSCR_DICT: return 2; case _BINARY_OP_SUBSCR_CHECK_FUNC: @@ -802,12 +6483,16 @@ int _PyUop_num_popped(int opcode, int oparg) return 3; case _STORE_SUBSCR_DICT: return 3; + case _STORE_SUBSCR_DICT_KNOWN_HASH: + return 3; case _DELETE_SUBSCR: return 2; case _CALL_INTRINSIC_1: return 1; case _CALL_INTRINSIC_2: return 2; + case _MAKE_HEAP_SAFE: + return 0; case _RETURN_VALUE: return 1; case _GET_AITER: @@ -818,6 +6503,16 @@ int _PyUop_num_popped(int opcode, int oparg) return 1; case _SEND_GEN_FRAME: return 1; + case _GUARD_TOS_IS_NONE: + return 0; + case _GUARD_NOS_NOT_NULL: + return 0; + case _SEND_VIRTUAL_TIER_TWO: + return 1; + case _GUARD_3OS_ASYNC_GEN_ASEND: + return 0; + case _SEND_ASYNC_GEN_TIER_TWO: + return 3; case _YIELD_VALUE: return 1; case _POP_EXCEPT: @@ -834,8 +6529,14 @@ int _PyUop_num_popped(int opcode, int oparg) return 1; case _UNPACK_SEQUENCE_TWO_TUPLE: return 1; + case _UNPACK_SEQUENCE_UNIQUE_TWO_TUPLE: + return 1; + case _UNPACK_SEQUENCE_UNIQUE_THREE_TUPLE: + return 1; case _UNPACK_SEQUENCE_TUPLE: return 1; + case _UNPACK_SEQUENCE_UNIQUE_TUPLE: + return 1; case _UNPACK_SEQUENCE_LIST: return 1; case _UNPACK_EX: @@ -850,6 +6551,8 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _LOAD_LOCALS: return 0; + case _LOAD_FROM_DICT_OR_GLOBALS: + return 1; case _LOAD_NAME: return 0; case _LOAD_GLOBAL: @@ -904,13 +6607,19 @@ int _PyUop_num_popped(int opcode, int oparg) return 2; case _LOAD_SUPER_ATTR_ATTR: return 3; + case _GUARD_NOS_TYPE_VERSION: + return 0; + case _GUARD_LOAD_SUPER_ATTR_METHOD: + return 0; case _LOAD_SUPER_ATTR_METHOD: return 3; case _LOAD_ATTR: return 1; case _GUARD_TYPE_VERSION: return 0; - case _GUARD_TYPE_VERSION_AND_LOCK: + case _GUARD_TYPE_VERSION_LOCKED: + return 0; + case _GUARD_TYPE: return 0; case _CHECK_MANAGED_OBJECT_HAS_VALUES: return 0; @@ -928,10 +6637,14 @@ int _PyUop_num_popped(int opcode, int oparg) return 1; case _LOAD_ATTR_PROPERTY_FRAME: return 1; + case _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN_FRAME: + return 1; case _GUARD_DORV_NO_DICT: return 0; case _STORE_ATTR_INSTANCE_VALUE: return 2; + case _LOCK_OBJECT: + return 0; case _STORE_ATTR_WITH_HINT: return 2; case _STORE_ATTR_SLOT: @@ -950,6 +6663,10 @@ int _PyUop_num_popped(int opcode, int oparg) return 2; case _GUARD_TOS_ANY_SET: return 0; + case _GUARD_TOS_SET: + return 0; + case _GUARD_TOS_FROZENSET: + return 0; case _CONTAINS_OP_SET: return 2; case _CONTAINS_OP_DICT: @@ -976,10 +6693,26 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _GET_ITER: return 1; - case _GET_YIELD_FROM_ITER: + case _GUARD_ITERATOR: + return 0; + case _GUARD_ITER_VIRTUAL: + return 0; + case _PUSH_TAGGED_ZERO: + return 0; + case _GET_ITER_TRAD: return 1; case _FOR_ITER_TIER_TWO: return 0; + case _GUARD_TYPE_ITER: + return 0; + case _ITER_NEXT_INLINE: + return 0; + case _GUARD_NOS_ITER_VIRTUAL: + return 0; + case _GUARD_TOS_NOT_NULL: + return 0; + case _FOR_ITER_VIRTUAL_TIER_TWO: + return 0; case _ITER_CHECK_LIST: return 0; case _GUARD_NOT_EXHAUSTED_LIST: @@ -1068,6 +6801,8 @@ int _PyUop_num_popped(int opcode, int oparg) return 1; case _GUARD_NOS_NULL: return 0; + case _GUARD_THIRD_NULL: + return 0; case _GUARD_CALLABLE_TYPE_1: return 0; case _CALL_TYPE_1: @@ -1080,36 +6815,68 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _CALL_TUPLE_1: return 3; - case _CHECK_AND_ALLOCATE_OBJECT: + case _CHECK_OBJECT: + return 0; + case _ALLOCATE_OBJECT: return 0; case _CREATE_INIT_FRAME: return 2 + oparg; case _EXIT_INIT_CHECK: return 1; + case _GUARD_CALLABLE_BUILTIN_CLASS: + return 0; case _CALL_BUILTIN_CLASS: - return 2 + oparg; + return 0; + case _GUARD_CALLABLE_BUILTIN_O: + return 0; case _CALL_BUILTIN_O: return 2 + oparg; + case _GUARD_CALLABLE_BUILTIN_FAST: + return 0; case _CALL_BUILTIN_FAST: - return 2 + oparg; + return 0; + case _GUARD_CALLABLE_BUILTIN_FAST_WITH_KEYWORDS: + return 0; case _CALL_BUILTIN_FAST_WITH_KEYWORDS: - return 2 + oparg; + return 0; case _GUARD_CALLABLE_LEN: return 0; case _CALL_LEN: return 3; + case _GUARD_CALLABLE_ISINSTANCE: + return 0; case _CALL_ISINSTANCE: - return 2 + oparg; + return 4; + case _GUARD_CALLABLE_LIST_APPEND: + return 0; case _CALL_LIST_APPEND: return 3; + case _GUARD_CALLABLE_METHOD_DESCRIPTOR_O: + return 0; case _CALL_METHOD_DESCRIPTOR_O: return 2 + oparg; + case _CHECK_RECURSION_LIMIT: + return 0; + case _CALL_METHOD_DESCRIPTOR_O_INLINE: + return 1 + oparg; + case _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS: + return 0; case _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS: - return 2 + oparg; + return 0; + case _CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS_INLINE: + return 0; + case _GUARD_CALLABLE_METHOD_DESCRIPTOR_NOARGS: + return 0; case _CALL_METHOD_DESCRIPTOR_NOARGS: return 2 + oparg; + case _CALL_METHOD_DESCRIPTOR_NOARGS_INLINE: + return 1 + oparg; + case _GUARD_CALLABLE_METHOD_DESCRIPTOR_FAST: + return 0; case _CALL_METHOD_DESCRIPTOR_FAST: - return 2 + oparg; + return 0; + case _CALL_METHOD_DESCRIPTOR_FAST_INLINE: + return 0; case _MAYBE_EXPAND_METHOD_KW: return 0; case _PY_FRAME_KW: @@ -1126,6 +6893,14 @@ int _PyUop_num_popped(int opcode, int oparg) return 3 + oparg; case _MAKE_CALLARGS_A_TUPLE: return 0; + case _CHECK_IS_PY_CALLABLE_EX: + return 0; + case _PY_FRAME_EX: + return 4; + case _CHECK_IS_NOT_PY_CALLABLE_EX: + return 0; + case _CALL_FUNCTION_EX_NON_PY_GENERAL: + return 4; case _MAKE_FUNCTION: return 1; case _SET_FUNCTION_ATTRIBUTE: @@ -1140,16 +6915,46 @@ int _PyUop_num_popped(int opcode, int oparg) return 1; case _FORMAT_WITH_SPEC: return 2; + case _COPY_1: + return 0; + case _COPY_2: + return 0; + case _COPY_3: + return 0; case _COPY: return 0; case _BINARY_OP: return 2; + case _SWAP_2: + return 0; + case _SWAP_3: + return 0; case _SWAP: return 0; case _GUARD_IS_TRUE_POP: return 1; case _GUARD_IS_FALSE_POP: return 1; + case _GUARD_BIT_IS_SET_POP_4: + return 1; + case _GUARD_BIT_IS_SET_POP_5: + return 1; + case _GUARD_BIT_IS_SET_POP_6: + return 1; + case _GUARD_BIT_IS_SET_POP_7: + return 1; + case _GUARD_BIT_IS_SET_POP: + return 1; + case _GUARD_BIT_IS_UNSET_POP_4: + return 1; + case _GUARD_BIT_IS_UNSET_POP_5: + return 1; + case _GUARD_BIT_IS_UNSET_POP_6: + return 1; + case _GUARD_BIT_IS_UNSET_POP_7: + return 1; + case _GUARD_BIT_IS_UNSET_POP: + return 1; case _GUARD_IS_NONE_POP: return 1; case _GUARD_IS_NOT_NONE_POP: @@ -1164,19 +6969,15 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _EXIT_TRACE: return 0; + case _DYNAMIC_EXIT: + return 0; case _CHECK_VALIDITY: return 0; case _LOAD_CONST_INLINE: return 0; - case _POP_TOP_LOAD_CONST_INLINE: - return 1; case _LOAD_CONST_INLINE_BORROW: return 0; - case _POP_TOP_LOAD_CONST_INLINE_BORROW: - return 1; - case _POP_TWO_LOAD_CONST_INLINE_BORROW: - return 2; - case _CHECK_FUNCTION: + case _RROT_3: return 0; case _START_EXECUTOR: return 0; @@ -1186,10 +6987,56 @@ int _PyUop_num_popped(int opcode, int oparg) return 0; case _DEOPT: return 0; + case _HANDLE_PENDING_AND_DEOPT: + return 0; case _ERROR_POP_N: return 0; + case _SPILL_OR_RELOAD: + return 0; case _TIER2_RESUME_CHECK: return 0; + case _COLD_EXIT: + return 0; + case _COLD_DYNAMIC_EXIT: + return 0; + case _GUARD_CODE_VERSION__PUSH_FRAME: + return 0; + case _GUARD_CODE_VERSION_YIELD_VALUE: + return 0; + case _GUARD_CODE_VERSION_RETURN_VALUE: + return 0; + case _GUARD_CODE_VERSION_RETURN_GENERATOR: + return 0; + case _GUARD_IP__PUSH_FRAME: + return 0; + case _GUARD_IP_YIELD_VALUE: + return 0; + case _GUARD_IP_RETURN_VALUE: + return 0; + case _GUARD_IP_RETURN_GENERATOR: + return 0; + case _RECORD_TOS: + return 0; + case _RECORD_TOS_TYPE: + return 0; + case _RECORD_NOS: + return 0; + case _RECORD_NOS_TYPE: + return 0; + case _RECORD_NOS_GEN_FUNC: + return 0; + case _RECORD_3OS_GEN_FUNC: + return 0; + case _RECORD_4OS: + return 0; + case _RECORD_CALLABLE: + return 0; + case _RECORD_CALLABLE_KW: + return 0; + case _RECORD_BOUND_METHOD: + return 0; + case _RECORD_CODE: + return 0; default: return -1; } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/lock.h b/python-multiplatform/src/nativeInterop/cinterop/include/lock.h deleted file mode 100644 index 782b9dbc7..000000000 --- a/python-multiplatform/src/nativeInterop/cinterop/include/lock.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef Py_LOCK_H -#define Py_LOCK_H -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef Py_LIMITED_API -# define Py_CPYTHON_LOCK_H -# include "cpython/lock.h" -# undef Py_CPYTHON_LOCK_H -#endif - -#ifdef __cplusplus -} -#endif -#endif /* !Py_LOCK_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/longobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/longobject.h index 19f069770..38673bc18 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/longobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/longobject.h @@ -166,6 +166,44 @@ PyAPI_FUNC(PyObject *) PyLong_FromString(const char *, char **, int); PyAPI_FUNC(unsigned long) PyOS_strtoul(const char *, char **, int); PyAPI_FUNC(long) PyOS_strtol(const char *, char **, int); +/* --- Import/Export API -------------------------------------------------- */ + +typedef struct PyLongLayout { + uint8_t bits_per_digit; + uint8_t digit_size; + int8_t digits_order; + int8_t digit_endianness; +} PyLongLayout; + +PyAPI_FUNC(const PyLongLayout*) PyLong_GetNativeLayout(void); + +typedef struct PyLongExport { + int64_t value; + uint8_t negative; + Py_ssize_t ndigits; + const void *digits; + // Member used internally, must not be used for other purpose. + Py_uintptr_t _reserved; +} PyLongExport; + +PyAPI_FUNC(int) PyLong_Export( + PyObject *obj, + PyLongExport *export_long); +PyAPI_FUNC(void) PyLong_FreeExport( + PyLongExport *export_long); + + +/* --- PyLongWriter API --------------------------------------------------- */ + +typedef struct PyLongWriter PyLongWriter; + +PyAPI_FUNC(PyLongWriter*) PyLongWriter_Create( + int negative, + Py_ssize_t ndigits, + void **digits); +PyAPI_FUNC(PyObject*) PyLongWriter_Finish(PyLongWriter *writer); +PyAPI_FUNC(void) PyLongWriter_Discard(PyLongWriter *writer); + #ifndef Py_LIMITED_API # define Py_CPYTHON_LONGOBJECT_H # include "cpython/longobject.h" diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/marshal.h b/python-multiplatform/src/nativeInterop/cinterop/include/marshal.h index f773587bd..2ccb112b4 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/marshal.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/marshal.h @@ -1,31 +1,18 @@ - /* Interface for marshal.c */ #ifndef Py_MARSHAL_H #define Py_MARSHAL_H -#ifndef Py_LIMITED_API - #ifdef __cplusplus extern "C" { #endif -PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromString(const char *, - Py_ssize_t); -PyAPI_FUNC(PyObject *) PyMarshal_WriteObjectToString(PyObject *, int); - -#define Py_MARSHAL_VERSION 5 - -PyAPI_FUNC(long) PyMarshal_ReadLongFromFile(FILE *); -PyAPI_FUNC(int) PyMarshal_ReadShortFromFile(FILE *); -PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromFile(FILE *); -PyAPI_FUNC(PyObject *) PyMarshal_ReadLastObjectFromFile(FILE *); - -PyAPI_FUNC(void) PyMarshal_WriteLongToFile(long, FILE *, int); -PyAPI_FUNC(void) PyMarshal_WriteObjectToFile(PyObject *, FILE *, int); +#ifndef Py_LIMITED_API +# define _Py_CPYTHON_MARSHAL_H +# include "cpython/marshal.h" +# undef _Py_CPYTHON_MARSHAL_H +#endif #ifdef __cplusplus } #endif - -#endif /* Py_LIMITED_API */ #endif /* !Py_MARSHAL_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/modsupport.h b/python-multiplatform/src/nativeInterop/cinterop/include/modsupport.h index af995f567..504a19d34 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/modsupport.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/modsupport.h @@ -56,58 +56,6 @@ PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def); #define Py_CLEANUP_SUPPORTED 0x20000 -#define PYTHON_API_VERSION 1013 -#define PYTHON_API_STRING "1013" -/* The API version is maintained (independently from the Python version) - so we can detect mismatches between the interpreter and dynamically - loaded modules. These are diagnosed by an error message but - the module is still loaded (because the mismatch can only be tested - after loading the module). The error message is intended to - explain the core dump a few seconds later. - - The symbol PYTHON_API_STRING defines the same value as a string - literal. *** PLEASE MAKE SURE THE DEFINITIONS MATCH. *** - - Please add a line or two to the top of this log for each API - version change: - - 22-Feb-2006 MvL 1013 PEP 353 - long indices for sequence lengths - - 19-Aug-2002 GvR 1012 Changes to string object struct for - interning changes, saving 3 bytes. - - 17-Jul-2001 GvR 1011 Descr-branch, just to be on the safe side - - 25-Jan-2001 FLD 1010 Parameters added to PyCode_New() and - PyFrame_New(); Python 2.1a2 - - 14-Mar-2000 GvR 1009 Unicode API added - - 3-Jan-1999 GvR 1007 Decided to change back! (Don't reuse 1008!) - - 3-Dec-1998 GvR 1008 Python 1.5.2b1 - - 18-Jan-1997 GvR 1007 string interning and other speedups - - 11-Oct-1996 GvR renamed Py_Ellipses to Py_Ellipsis :-( - - 30-Jul-1996 GvR Slice and ellipses syntax added - - 23-Jul-1996 GvR For 1.4 -- better safe than sorry this time :-) - - 7-Nov-1995 GvR Keyword arguments (should've been done at 1.3 :-( ) - - 10-Jan-1995 GvR Renamed globals to new naming scheme - - 9-Jan-1995 GvR Initial version (incompatible with older API) -*/ - -/* The PYTHON_ABI_VERSION is introduced in PEP 384. For the lifetime of - Python 3, it will stay at the value of 3; changes to the limited API - must be performed in a strictly backwards-compatible manner. */ -#define PYTHON_ABI_VERSION 3 -#define PYTHON_ABI_STRING "3" - PyAPI_FUNC(PyObject *) PyModule_Create2(PyModuleDef*, int apiver); #ifdef Py_LIMITED_API @@ -134,6 +82,72 @@ PyAPI_FUNC(PyObject *) PyModule_FromDefAndSpec2(PyModuleDef *def, #endif /* New in 3.5 */ +/* ABI info & checking (new in 3.15) */ +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +typedef struct PyABIInfo { + uint8_t abiinfo_major_version; + uint8_t abiinfo_minor_version; + uint16_t flags; + uint32_t build_version; + uint32_t abi_version; +} PyABIInfo; +#define PyABIInfo_STABLE 0x0001 +#define PyABIInfo_GIL 0x0002 +#define PyABIInfo_FREETHREADED 0x0004 +#define PyABIInfo_INTERNAL 0x0008 + +#define PyABIInfo_FREETHREADING_AGNOSTIC (PyABIInfo_GIL|PyABIInfo_FREETHREADED) + +PyAPI_FUNC(int) PyABIInfo_Check(PyABIInfo *info, const char *module_name); + +// Define the defaults +#ifdef Py_LIMITED_API + #define _PyABIInfo_DEFAULT_FLAG_STABLE PyABIInfo_STABLE + #if Py_LIMITED_API == 3 + #define PyABIInfo_DEFAULT_ABI_VERSION _Py_PACK_VERSION(3, 2) + #else + #define PyABIInfo_DEFAULT_ABI_VERSION Py_LIMITED_API + #endif +#else + #define _PyABIInfo_DEFAULT_FLAG_STABLE 0 + #define PyABIInfo_DEFAULT_ABI_VERSION PY_VERSION_HEX +#endif +#if defined(Py_LIMITED_API) && defined(_Py_OPAQUE_PYOBJECT) + #define _PyABIInfo_DEFAULT_FLAG_FT PyABIInfo_FREETHREADING_AGNOSTIC +#elif defined(Py_GIL_DISABLED) + #define _PyABIInfo_DEFAULT_FLAG_FT PyABIInfo_FREETHREADED +#else + #define _PyABIInfo_DEFAULT_FLAG_FT PyABIInfo_GIL +#endif +#if defined(Py_BUILD_CORE) + #define _PyABIInfo_DEFAULT_FLAG_INTERNAL PyABIInfo_INTERNAL +#else + #define _PyABIInfo_DEFAULT_FLAG_INTERNAL 0 +#endif + +#define PyABIInfo_DEFAULT_FLAGS ( \ + _PyABIInfo_DEFAULT_FLAG_STABLE \ + | _PyABIInfo_DEFAULT_FLAG_FT \ + | _PyABIInfo_DEFAULT_FLAG_INTERNAL \ + ) \ + ///////////////////////////////////////////////////////// + +#define _PyABIInfo_DEFAULT { \ + 1, 0, \ + PyABIInfo_DEFAULT_FLAGS, \ + PY_VERSION_HEX, \ + PyABIInfo_DEFAULT_ABI_VERSION } \ + ///////////////////////////////////////////////////////// + +#define PyABIInfo_VAR(NAME) \ + static PyABIInfo NAME = _PyABIInfo_DEFAULT + +#undef _PyABIInfo_DEFAULT_STABLE +#undef _PyABIInfo_DEFAULT_FT +#undef _PyABIInfo_DEFAULT_INTERNAL + +#endif /* ABI info (new in 3.15) */ + #ifndef Py_LIMITED_API # define Py_CPYTHON_MODSUPPORT_H # include "cpython/modsupport.h" diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/moduleobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/moduleobject.h index 2a17c891d..88c66672f 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/moduleobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/moduleobject.h @@ -36,6 +36,7 @@ PyAPI_FUNC(PyObject *) PyModuleDef_Init(PyModuleDef*); PyAPI_DATA(PyTypeObject) PyModuleDef_Type; #endif +#ifndef _Py_OPAQUE_PYOBJECT typedef struct PyModuleDef_Base { PyObject_HEAD /* The function used to re-initialize the module. @@ -63,6 +64,7 @@ typedef struct PyModuleDef_Base { 0, /* m_index */ \ _Py_NULL, /* m_copy */ \ } +#endif // _Py_OPAQUE_PYOBJECT #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 /* New in 3.5 */ @@ -71,20 +73,6 @@ struct PyModuleDef_Slot { void *value; }; -#define Py_mod_create 1 -#define Py_mod_exec 2 -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030c0000 -# define Py_mod_multiple_interpreters 3 -#endif -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000 -# define Py_mod_gil 4 -#endif - - -#ifndef Py_LIMITED_API -#define _Py_mod_LAST_SLOT 4 -#endif - #endif /* New in 3.5 */ /* for Py_mod_multiple_interpreters: */ @@ -100,10 +88,23 @@ struct PyModuleDef_Slot { # define Py_MOD_GIL_NOT_USED ((void *)1) #endif -#if !defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED) +#if !defined(Py_LIMITED_API) +# if defined(Py_GIL_DISABLED) PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); +# endif +#endif + +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +PyAPI_FUNC(PyObject *) PyModule_FromSlotsAndSpec(const PySlot *slots, + PyObject *spec); +PyAPI_FUNC(int) PyModule_Exec(PyObject *module); +PyAPI_FUNC(int) PyModule_GetStateSize(PyObject *module, Py_ssize_t *result); +PyAPI_FUNC(int) PyModule_GetToken(PyObject *module, void **result); +PyAPI_FUNC(void*) PyModule_GetState_DuringGC(PyObject*); +PyAPI_FUNC(int) PyModule_GetToken_DuringGC(PyObject *module, void **result); #endif +#ifndef _Py_OPAQUE_PYOBJECT struct PyModuleDef { PyModuleDef_Base m_base; const char* m_name; @@ -115,6 +116,7 @@ struct PyModuleDef { inquiry m_clear; freefunc m_free; }; +#endif // _Py_OPAQUE_PYOBJECT #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/monitoring.h b/python-multiplatform/src/nativeInterop/cinterop/include/monitoring.h deleted file mode 100644 index 985f7f230..000000000 --- a/python-multiplatform/src/nativeInterop/cinterop/include/monitoring.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef Py_MONITORING_H -#define Py_MONITORING_H -#ifdef __cplusplus -extern "C" { -#endif - -// There is currently no limited API for monitoring - -#ifndef Py_LIMITED_API -# define Py_CPYTHON_MONITORING_H -# include "cpython/monitoring.h" -# undef Py_CPYTHON_MONITORING_H -#endif - -#ifdef __cplusplus -} -#endif -#endif /* !Py_MONITORING_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/object.h b/python-multiplatform/src/nativeInterop/cinterop/include/object.h index 50a41e355..20c2dab44 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/object.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/object.h @@ -56,6 +56,11 @@ whose size is determined when the object is allocated. # define Py_REF_DEBUG #endif +#if defined(_Py_OPAQUE_PYOBJECT) && !defined(Py_LIMITED_API) +# error "_Py_OPAQUE_PYOBJECT only makes sense with Py_LIMITED_API" +#endif + +#ifndef _Py_OPAQUE_PYOBJECT /* PyObject_HEAD defines the initial segment of every PyObject. */ #define PyObject_HEAD PyObject ob_base; @@ -66,6 +71,8 @@ whose size is determined when the object is allocated. * * Statically allocated objects might be shared between * interpreters, so must be marked as immortal. + * + * Before changing this, see the check in PyModuleDef_Init(). */ #if defined(Py_GIL_DISABLED) #define PyObject_HEAD_INIT(type) \ @@ -99,29 +106,28 @@ whose size is determined when the object is allocated. * not necessarily a byte count. */ #define PyObject_VAR_HEAD PyVarObject ob_base; +#endif // !defined(_Py_OPAQUE_PYOBJECT) + #define Py_INVALID_SIZE (Py_ssize_t)-1 +/* PyObjects are given a minimum alignment so that the least significant bits + * of an object pointer become available for other purposes. + * This must be an integer literal with the value (1 << _PyGC_PREV_SHIFT), number of bytes. + */ +#define _PyObject_MIN_ALIGNMENT 4 + /* Nothing is actually declared to be a PyObject, but every pointer to * a Python object can be cast to a PyObject*. This is inheritance built * by hand. Similarly every pointer to a variable-size Python object can, * in addition, be cast to PyVarObject*. */ -#ifndef Py_GIL_DISABLED +#ifdef _Py_OPAQUE_PYOBJECT + /* PyObject is opaque */ +#elif !defined(Py_GIL_DISABLED) struct _object { -#if (defined(__GNUC__) || defined(__clang__)) \ - && !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L) - // On C99 and older, anonymous union is a GCC and clang extension - __extension__ -#endif -#ifdef _MSC_VER - // Ignore MSC warning C4201: "nonstandard extension used: - // nameless struct/union" - __pragma(warning(push)) - __pragma(warning(disable: 4201)) -#endif - union { + _Py_ANONYMOUS union { #if SIZEOF_VOID_P > 4 - PY_INT64_T ob_refcnt_full; /* This field is needed for efficient initialization with Clang on ARM */ + int64_t ob_refcnt_full; /* This field is needed for efficient initialization with Clang on ARM */ struct { # if PY_BIG_ENDIAN uint16_t ob_flags; @@ -134,14 +140,12 @@ struct _object { # endif }; #else - Py_ssize_t ob_refcnt; + Py_ssize_t ob_refcnt; // part of stable ABI; do not change #endif + _Py_ALIGNED_DEF(_PyObject_MIN_ALIGNMENT, char) _aligner; }; -#ifdef _MSC_VER - __pragma(warning(pop)) -#endif - PyTypeObject *ob_type; + PyTypeObject *ob_type; // part of stable ABI; do not change }; #else // Objects that are not owned by any thread use a thread id (tid) of zero. @@ -153,7 +157,7 @@ struct _object { // ob_tid stores the thread id (or zero). It is also used by the GC and the // trashcan mechanism as a linked list pointer and by the GC to store the // computed "gc_refs" refcount. - uintptr_t ob_tid; + _Py_ALIGNED_DEF(_PyObject_MIN_ALIGNMENT, uintptr_t) ob_tid; uint16_t ob_flags; PyMutex ob_mutex; // per-object lock uint8_t ob_gc_bits; // gc-related state @@ -161,15 +165,18 @@ struct _object { Py_ssize_t ob_ref_shared; // shared (atomic) reference count PyTypeObject *ob_type; }; -#endif +#endif // !defined(_Py_OPAQUE_PYOBJECT) /* Cast argument to PyObject* type. */ #define _PyObject_CAST(op) _Py_CAST(PyObject*, (op)) -typedef struct { +#ifndef _Py_OPAQUE_PYOBJECT +struct PyVarObject { PyObject ob_base; - Py_ssize_t ob_size; /* Number of items in variable part */ -} PyVarObject; + Py_ssize_t ob_size; // Number of items in variable part. Part of stable ABI +}; +#endif +typedef struct PyVarObject PyVarObject; /* Cast argument to PyVarObject* type. */ #define _PyVarObject_CAST(op) _Py_CAST(PyVarObject*, (op)) @@ -179,132 +186,72 @@ typedef struct { PyAPI_FUNC(int) Py_Is(PyObject *x, PyObject *y); #define Py_Is(x, y) ((x) == (y)) -#if defined(Py_GIL_DISABLED) && !defined(Py_LIMITED_API) -PyAPI_FUNC(uintptr_t) _Py_GetThreadLocal_Addr(void); +PyAPI_DATA(PyTypeObject) PyLong_Type; +PyAPI_DATA(PyTypeObject) PyBool_Type; -static inline uintptr_t -_Py_ThreadId(void) -{ - uintptr_t tid; -#if defined(_MSC_VER) && defined(_M_X64) - tid = __readgsqword(48); -#elif defined(_MSC_VER) && defined(_M_IX86) - tid = __readfsdword(24); -#elif defined(_MSC_VER) && defined(_M_ARM64) - tid = __getReg(18); -#elif defined(__MINGW32__) && defined(_M_X64) - tid = __readgsqword(48); -#elif defined(__MINGW32__) && defined(_M_IX86) - tid = __readfsdword(24); -#elif defined(__MINGW32__) && defined(_M_ARM64) - tid = __getReg(18); -#elif defined(__i386__) - __asm__("{movl %%gs:0, %0|mov %0, dword ptr gs:[0]}" : "=r" (tid)); // 32-bit always uses GS -#elif defined(__MACH__) && defined(__x86_64__) - __asm__("{movq %%gs:0, %0|mov %0, qword ptr gs:[0]}" : "=r" (tid)); // x86_64 macOSX uses GS -#elif defined(__x86_64__) - __asm__("{movq %%fs:0, %0|mov %0, qword ptr fs:[0]}" : "=r" (tid)); // x86_64 Linux, BSD uses FS -#elif defined(__arm__) && __ARM_ARCH >= 7 - __asm__ ("mrc p15, 0, %0, c13, c0, 3\nbic %0, %0, #3" : "=r" (tid)); -#elif defined(__aarch64__) && defined(__APPLE__) - __asm__ ("mrs %0, tpidrro_el0" : "=r" (tid)); -#elif defined(__aarch64__) - __asm__ ("mrs %0, tpidr_el0" : "=r" (tid)); -#elif defined(__powerpc64__) - #if defined(__clang__) && _Py__has_builtin(__builtin_thread_pointer) - tid = (uintptr_t)__builtin_thread_pointer(); - #else - // r13 is reserved for use as system thread ID by the Power 64-bit ABI. - register uintptr_t tp __asm__ ("r13"); - __asm__("" : "=r" (tp)); - tid = tp; - #endif -#elif defined(__powerpc__) - #if defined(__clang__) && _Py__has_builtin(__builtin_thread_pointer) - tid = (uintptr_t)__builtin_thread_pointer(); - #else - // r2 is reserved for use as system thread ID by the Power 32-bit ABI. - register uintptr_t tp __asm__ ("r2"); - __asm__ ("" : "=r" (tp)); - tid = tp; - #endif -#elif defined(__s390__) && defined(__GNUC__) - // Both GCC and Clang have supported __builtin_thread_pointer - // for s390 from long time ago. - tid = (uintptr_t)__builtin_thread_pointer(); -#elif defined(__riscv) - #if defined(__clang__) && _Py__has_builtin(__builtin_thread_pointer) - tid = (uintptr_t)__builtin_thread_pointer(); - #else - // tp is Thread Pointer provided by the RISC-V ABI. - __asm__ ("mv %0, tp" : "=r" (tid)); - #endif -#else - // Fallback to a portable implementation if we do not have a faster - // platform-specific implementation. - tid = _Py_GetThreadLocal_Addr(); +/* Definitions for the stable ABI */ +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 14) +PyAPI_FUNC(PyTypeObject*) Py_TYPE(PyObject *ob); +#endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +PyAPI_FUNC(Py_ssize_t) Py_SIZE(PyObject *ob); +PyAPI_FUNC(int) Py_IS_TYPE(PyObject *ob, PyTypeObject *type); +PyAPI_FUNC(void) Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size); #endif - return tid; -} -static inline Py_ALWAYS_INLINE int -_Py_IsOwnedByCurrentThread(PyObject *ob) +#ifndef _Py_OPAQUE_PYOBJECT + +static inline void +Py_SET_TYPE(PyObject *ob, PyTypeObject *type) { -#ifdef _Py_THREAD_SANITIZER - return _Py_atomic_load_uintptr_relaxed(&ob->ob_tid) == _Py_ThreadId(); -#else - return ob->ob_tid == _Py_ThreadId(); -#endif + ob->ob_type = type; } -#endif - -// Py_TYPE() implementation for the stable ABI -PyAPI_FUNC(PyTypeObject*) Py_TYPE(PyObject *ob); -#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030e0000 - // Stable ABI implements Py_TYPE() as a function call - // on limited C API version 3.14 and newer. +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < _Py_PACK_VERSION(3, 11) +// Non-limited API & limited API 3.11 & below: use static inline functions and +// use _PyObject_CAST so that users don't need their own casts +# define Py_TYPE(ob) _Py_TYPE_impl(_PyObject_CAST(ob)) +# define Py_SIZE(ob) _Py_SIZE_impl(_PyObject_CAST(ob)) +# define Py_IS_TYPE(ob, type) _Py_IS_TYPE_impl(_PyObject_CAST(ob), (type)) +# define Py_SET_SIZE(ob, size) _Py_SET_SIZE_impl(_PyVarObject_CAST(ob), (size)) +# define Py_SET_TYPE(ob, type) Py_SET_TYPE(_PyObject_CAST(ob), type) +#elif Py_LIMITED_API+0 < _Py_PACK_VERSION(3, 15) +// Limited API 3.11-3.14: use static inline functions, without casts +# define Py_SIZE(ob) _Py_SIZE_impl(ob) +# define Py_IS_TYPE(ob, type) _Py_IS_TYPE_impl((ob), (type)) +# define Py_SET_SIZE(ob, size) _Py_SET_SIZE_impl((ob), (size)) +# if Py_LIMITED_API+0 < _Py_PACK_VERSION(3, 14) +// Py_TYPE() is static inline only on Limited API 3.13 and below +# define Py_TYPE(ob) _Py_TYPE_impl(ob) +# endif #else - static inline PyTypeObject* _Py_TYPE(PyObject *ob) - { - return ob->ob_type; - } - #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 - # define Py_TYPE(ob) _Py_TYPE(_PyObject_CAST(ob)) - #else - # define Py_TYPE(ob) _Py_TYPE(ob) - #endif +// Limited API 3.15+: use function calls #endif -PyAPI_DATA(PyTypeObject) PyLong_Type; -PyAPI_DATA(PyTypeObject) PyBool_Type; +static inline +PyTypeObject* _Py_TYPE_impl(PyObject *ob) +{ + return ob->ob_type; +} // bpo-39573: The Py_SET_SIZE() function must be used to set an object size. -static inline Py_ssize_t Py_SIZE(PyObject *ob) { +static inline Py_ssize_t +_Py_SIZE_impl(PyObject *ob) +{ assert(Py_TYPE(ob) != &PyLong_Type); assert(Py_TYPE(ob) != &PyBool_Type); return _PyVarObject_CAST(ob)->ob_size; } -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 -# define Py_SIZE(ob) Py_SIZE(_PyObject_CAST(ob)) -#endif -static inline int Py_IS_TYPE(PyObject *ob, PyTypeObject *type) { +static inline int +_Py_IS_TYPE_impl(PyObject *ob, PyTypeObject *type) +{ return Py_TYPE(ob) == type; } -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 -# define Py_IS_TYPE(ob, type) Py_IS_TYPE(_PyObject_CAST(ob), (type)) -#endif - - -static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) { - ob->ob_type = type; -} -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 -# define Py_SET_TYPE(ob, type) Py_SET_TYPE(_PyObject_CAST(ob), type) -#endif -static inline void Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) { +static inline void +_Py_SET_SIZE_impl(PyVarObject *ob, Py_ssize_t size) +{ assert(Py_TYPE(_PyObject_CAST(ob)) != &PyLong_Type); assert(Py_TYPE(_PyObject_CAST(ob)) != &PyBool_Type); #ifdef Py_GIL_DISABLED @@ -313,9 +260,8 @@ static inline void Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) { ob->ob_size = size; #endif } -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 -# define Py_SET_SIZE(ob, size) Py_SET_SIZE(_PyVarObject_CAST(ob), (size)) -#endif + +#endif // !defined(_Py_OPAQUE_PYOBJECT) /* @@ -360,6 +306,11 @@ typedef Py_hash_t (*hashfunc)(PyObject *); typedef PyObject *(*richcmpfunc) (PyObject *, PyObject *, int); typedef PyObject *(*getiterfunc) (PyObject *); typedef PyObject *(*iternextfunc) (PyObject *); +typedef struct { + PyObject *object; + Py_ssize_t index; +} _PyObjectIndexPair; +typedef _PyObjectIndexPair (*_Py_iteritemfunc) (PyObject *, Py_ssize_t index); typedef PyObject *(*descrgetfunc) (PyObject *, PyObject *, PyObject *); typedef int (*descrsetfunc) (PyObject *, PyObject *, PyObject *); typedef int (*initproc)(PyObject *, PyObject *, PyObject *); @@ -413,6 +364,9 @@ PyAPI_FUNC(Py_ssize_t) PyType_GetTypeDataSize(PyTypeObject *cls); PyAPI_FUNC(int) PyType_GetBaseByToken(PyTypeObject *, void *, PyTypeObject **); #define Py_TP_USE_SPEC NULL #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +PyAPI_FUNC(PyObject *) PyType_FromSlots(struct PySlot *slots); +#endif /* Generic type check */ PyAPI_FUNC(int) PyType_IsSubtype(PyTypeObject *, PyTypeObject *); @@ -518,7 +472,7 @@ given type object has a specified feature. #define Py_TPFLAGS_INLINE_VALUES (1 << 2) /* Placement of weakref pointers are managed by the VM, not by the type. - * The VM will automatically set tp_weaklistoffset. + * The VM will automatically set tp_weaklistoffset. Implies Py_TPFLAGS_HAVE_GC. */ #define Py_TPFLAGS_MANAGED_WEAKREF (1 << 3) @@ -527,6 +481,9 @@ given type object has a specified feature. */ #define Py_TPFLAGS_MANAGED_DICT (1 << 4) +/* Type has dictionary or weakref pointers that are managed by VM and has + * to allocate space to store these. + */ #define Py_TPFLAGS_PREHEADER (Py_TPFLAGS_MANAGED_WEAKREF | Py_TPFLAGS_MANAGED_DICT) /* Set if instances of the type object are treated as sequences for pattern matching */ @@ -620,6 +577,15 @@ given type object has a specified feature. #define Py_TPFLAGS_HAVE_FINALIZE (1UL << 0) #define Py_TPFLAGS_HAVE_VERSION_TAG (1UL << 18) +// Flag values for ob_flags (16 bits available, if SIZEOF_VOID_P > 4). +#define _Py_IMMORTAL_FLAGS (1 << 0) +#define _Py_LEGACY_ABI_CHECK_FLAG (1 << 1) /* see PyModuleDef_Init() */ +#define _Py_STATICALLY_ALLOCATED_FLAG (1 << 2) +#if !defined(Py_LIMITED_API) +# if defined(Py_GIL_DISABLED) && defined(Py_DEBUG) +# define _Py_TYPE_REVEALED_FLAG (1 << 3) +# endif +#endif #define Py_CONSTANT_NONE 0 #define Py_CONSTANT_FALSE 1 @@ -786,11 +752,7 @@ PyType_HasFeature(PyTypeObject *type, unsigned long feature) // PyTypeObject is opaque in the limited C API flags = PyType_GetFlags(type); #else -# ifdef Py_GIL_DISABLED - flags = _Py_atomic_load_ulong_relaxed(&type->tp_flags); -# else - flags = type->tp_flags; -# endif + flags = type->tp_flags; #endif return ((flags & feature) != 0); } @@ -822,6 +784,20 @@ PyAPI_FUNC(PyObject *) PyType_GetModuleByDef(PyTypeObject *, PyModuleDef *); PyAPI_FUNC(int) PyType_Freeze(PyTypeObject *type); #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +PyAPI_FUNC(int) PyObject_CallFinalizerFromDealloc(PyObject *); +PyAPI_FUNC(PyObject *) PyType_GetModuleByToken(PyTypeObject *type, + const void *token); +PyAPI_FUNC(void *) PyObject_GetTypeData_DuringGC(PyObject *obj, + PyTypeObject *cls); +PyAPI_FUNC(void *) PyType_GetModuleState_DuringGC(PyTypeObject *); +PyAPI_FUNC(int) PyType_GetBaseByToken_DuringGC(PyTypeObject *, + void *, PyTypeObject **); +PyAPI_FUNC(PyObject *) PyType_GetModule_DuringGC(PyTypeObject *); +PyAPI_FUNC(PyObject *) PyType_GetModuleByToken_DuringGC(PyTypeObject *type, + const void *token); +#endif + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/opcode_ids.h b/python-multiplatform/src/nativeInterop/cinterop/include/opcode_ids.h index 209103c83..758542720 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/opcode_ids.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/opcode_ids.h @@ -26,111 +26,110 @@ extern "C" { #define FORMAT_WITH_SPEC 13 #define GET_AITER 14 #define GET_ANEXT 15 -#define GET_ITER 16 +#define GET_LEN 16 #define RESERVED 17 -#define GET_LEN 18 -#define GET_YIELD_FROM_ITER 19 -#define INTERPRETER_EXIT 20 -#define LOAD_BUILD_CLASS 21 -#define LOAD_LOCALS 22 -#define MAKE_FUNCTION 23 -#define MATCH_KEYS 24 -#define MATCH_MAPPING 25 -#define MATCH_SEQUENCE 26 -#define NOP 27 -#define NOT_TAKEN 28 -#define POP_EXCEPT 29 -#define POP_ITER 30 -#define POP_TOP 31 -#define PUSH_EXC_INFO 32 -#define PUSH_NULL 33 -#define RETURN_GENERATOR 34 -#define RETURN_VALUE 35 -#define SETUP_ANNOTATIONS 36 -#define STORE_SLICE 37 -#define STORE_SUBSCR 38 -#define TO_BOOL 39 -#define UNARY_INVERT 40 -#define UNARY_NEGATIVE 41 -#define UNARY_NOT 42 -#define WITH_EXCEPT_START 43 -#define BINARY_OP 44 -#define BUILD_INTERPOLATION 45 -#define BUILD_LIST 46 -#define BUILD_MAP 47 -#define BUILD_SET 48 -#define BUILD_SLICE 49 -#define BUILD_STRING 50 -#define BUILD_TUPLE 51 -#define CALL 52 -#define CALL_INTRINSIC_1 53 -#define CALL_INTRINSIC_2 54 -#define CALL_KW 55 -#define COMPARE_OP 56 -#define CONTAINS_OP 57 -#define CONVERT_VALUE 58 -#define COPY 59 -#define COPY_FREE_VARS 60 -#define DELETE_ATTR 61 -#define DELETE_DEREF 62 -#define DELETE_FAST 63 -#define DELETE_GLOBAL 64 -#define DELETE_NAME 65 -#define DICT_MERGE 66 -#define DICT_UPDATE 67 -#define END_ASYNC_FOR 68 -#define EXTENDED_ARG 69 -#define FOR_ITER 70 -#define GET_AWAITABLE 71 -#define IMPORT_FROM 72 -#define IMPORT_NAME 73 -#define IS_OP 74 -#define JUMP_BACKWARD 75 -#define JUMP_BACKWARD_NO_INTERRUPT 76 -#define JUMP_FORWARD 77 -#define LIST_APPEND 78 -#define LIST_EXTEND 79 -#define LOAD_ATTR 80 -#define LOAD_COMMON_CONSTANT 81 -#define LOAD_CONST 82 -#define LOAD_DEREF 83 -#define LOAD_FAST 84 -#define LOAD_FAST_AND_CLEAR 85 -#define LOAD_FAST_BORROW 86 -#define LOAD_FAST_BORROW_LOAD_FAST_BORROW 87 -#define LOAD_FAST_CHECK 88 -#define LOAD_FAST_LOAD_FAST 89 -#define LOAD_FROM_DICT_OR_DEREF 90 -#define LOAD_FROM_DICT_OR_GLOBALS 91 -#define LOAD_GLOBAL 92 -#define LOAD_NAME 93 -#define LOAD_SMALL_INT 94 -#define LOAD_SPECIAL 95 -#define LOAD_SUPER_ATTR 96 -#define MAKE_CELL 97 -#define MAP_ADD 98 -#define MATCH_CLASS 99 -#define POP_JUMP_IF_FALSE 100 -#define POP_JUMP_IF_NONE 101 -#define POP_JUMP_IF_NOT_NONE 102 -#define POP_JUMP_IF_TRUE 103 -#define RAISE_VARARGS 104 -#define RERAISE 105 -#define SEND 106 -#define SET_ADD 107 -#define SET_FUNCTION_ATTRIBUTE 108 -#define SET_UPDATE 109 -#define STORE_ATTR 110 -#define STORE_DEREF 111 -#define STORE_FAST 112 -#define STORE_FAST_LOAD_FAST 113 -#define STORE_FAST_STORE_FAST 114 -#define STORE_GLOBAL 115 -#define STORE_NAME 116 -#define SWAP 117 -#define UNPACK_EX 118 -#define UNPACK_SEQUENCE 119 -#define YIELD_VALUE 120 +#define INTERPRETER_EXIT 18 +#define LOAD_BUILD_CLASS 19 +#define LOAD_LOCALS 20 +#define MAKE_FUNCTION 21 +#define MATCH_KEYS 22 +#define MATCH_MAPPING 23 +#define MATCH_SEQUENCE 24 +#define NOP 25 +#define NOT_TAKEN 26 +#define POP_EXCEPT 27 +#define POP_ITER 28 +#define POP_TOP 29 +#define PUSH_EXC_INFO 30 +#define PUSH_NULL 31 +#define RETURN_GENERATOR 32 +#define RETURN_VALUE 33 +#define SETUP_ANNOTATIONS 34 +#define STORE_SLICE 35 +#define STORE_SUBSCR 36 +#define TO_BOOL 37 +#define UNARY_INVERT 38 +#define UNARY_NEGATIVE 39 +#define UNARY_NOT 40 +#define WITH_EXCEPT_START 41 +#define BINARY_OP 42 +#define BUILD_INTERPOLATION 43 +#define BUILD_LIST 44 +#define BUILD_MAP 45 +#define BUILD_SET 46 +#define BUILD_SLICE 47 +#define BUILD_STRING 48 +#define BUILD_TUPLE 49 +#define CALL 50 +#define CALL_INTRINSIC_1 51 +#define CALL_INTRINSIC_2 52 +#define CALL_KW 53 +#define COMPARE_OP 54 +#define CONTAINS_OP 55 +#define CONVERT_VALUE 56 +#define COPY 57 +#define COPY_FREE_VARS 58 +#define DELETE_ATTR 59 +#define DELETE_DEREF 60 +#define DELETE_FAST 61 +#define DELETE_GLOBAL 62 +#define DELETE_NAME 63 +#define DICT_MERGE 64 +#define DICT_UPDATE 65 +#define END_ASYNC_FOR 66 +#define EXTENDED_ARG 67 +#define FOR_ITER 68 +#define GET_AWAITABLE 69 +#define GET_ITER 70 +#define IMPORT_FROM 71 +#define IMPORT_NAME 72 +#define IS_OP 73 +#define JUMP_BACKWARD 74 +#define JUMP_BACKWARD_NO_INTERRUPT 75 +#define JUMP_FORWARD 76 +#define LIST_APPEND 77 +#define LIST_EXTEND 78 +#define LOAD_ATTR 79 +#define LOAD_COMMON_CONSTANT 80 +#define LOAD_CONST 81 +#define LOAD_DEREF 82 +#define LOAD_FAST 83 +#define LOAD_FAST_AND_CLEAR 84 +#define LOAD_FAST_BORROW 85 +#define LOAD_FAST_BORROW_LOAD_FAST_BORROW 86 +#define LOAD_FAST_CHECK 87 +#define LOAD_FAST_LOAD_FAST 88 +#define LOAD_FROM_DICT_OR_DEREF 89 +#define LOAD_FROM_DICT_OR_GLOBALS 90 +#define LOAD_GLOBAL 91 +#define LOAD_NAME 92 +#define LOAD_SMALL_INT 93 +#define LOAD_SPECIAL 94 +#define LOAD_SUPER_ATTR 95 +#define MAKE_CELL 96 +#define MAP_ADD 97 +#define MATCH_CLASS 98 +#define POP_JUMP_IF_FALSE 99 +#define POP_JUMP_IF_NONE 100 +#define POP_JUMP_IF_NOT_NONE 101 +#define POP_JUMP_IF_TRUE 102 +#define RAISE_VARARGS 103 +#define RERAISE 104 +#define SEND 105 +#define SET_ADD 106 +#define SET_FUNCTION_ATTRIBUTE 107 +#define SET_UPDATE 108 +#define STORE_ATTR 109 +#define STORE_DEREF 110 +#define STORE_FAST 111 +#define STORE_FAST_LOAD_FAST 112 +#define STORE_FAST_STORE_FAST 113 +#define STORE_GLOBAL 114 +#define STORE_NAME 115 +#define SWAP 116 +#define UNPACK_EX 117 +#define UNPACK_SEQUENCE 118 +#define YIELD_VALUE 119 #define RESUME 128 #define BINARY_OP_ADD_FLOAT 129 #define BINARY_OP_ADD_INT 130 @@ -144,99 +143,107 @@ extern "C" { #define BINARY_OP_SUBSCR_LIST_SLICE 138 #define BINARY_OP_SUBSCR_STR_INT 139 #define BINARY_OP_SUBSCR_TUPLE_INT 140 -#define BINARY_OP_SUBTRACT_FLOAT 141 -#define BINARY_OP_SUBTRACT_INT 142 -#define CALL_ALLOC_AND_ENTER_INIT 143 -#define CALL_BOUND_METHOD_EXACT_ARGS 144 -#define CALL_BOUND_METHOD_GENERAL 145 -#define CALL_BUILTIN_CLASS 146 -#define CALL_BUILTIN_FAST 147 -#define CALL_BUILTIN_FAST_WITH_KEYWORDS 148 -#define CALL_BUILTIN_O 149 -#define CALL_ISINSTANCE 150 -#define CALL_KW_BOUND_METHOD 151 -#define CALL_KW_NON_PY 152 -#define CALL_KW_PY 153 -#define CALL_LEN 154 -#define CALL_LIST_APPEND 155 -#define CALL_METHOD_DESCRIPTOR_FAST 156 -#define CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS 157 -#define CALL_METHOD_DESCRIPTOR_NOARGS 158 -#define CALL_METHOD_DESCRIPTOR_O 159 -#define CALL_NON_PY_GENERAL 160 -#define CALL_PY_EXACT_ARGS 161 -#define CALL_PY_GENERAL 162 -#define CALL_STR_1 163 -#define CALL_TUPLE_1 164 -#define CALL_TYPE_1 165 -#define COMPARE_OP_FLOAT 166 -#define COMPARE_OP_INT 167 -#define COMPARE_OP_STR 168 -#define CONTAINS_OP_DICT 169 -#define CONTAINS_OP_SET 170 -#define FOR_ITER_GEN 171 -#define FOR_ITER_LIST 172 -#define FOR_ITER_RANGE 173 -#define FOR_ITER_TUPLE 174 -#define JUMP_BACKWARD_JIT 175 -#define JUMP_BACKWARD_NO_JIT 176 -#define LOAD_ATTR_CLASS 177 -#define LOAD_ATTR_CLASS_WITH_METACLASS_CHECK 178 -#define LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN 179 -#define LOAD_ATTR_INSTANCE_VALUE 180 -#define LOAD_ATTR_METHOD_LAZY_DICT 181 -#define LOAD_ATTR_METHOD_NO_DICT 182 -#define LOAD_ATTR_METHOD_WITH_VALUES 183 -#define LOAD_ATTR_MODULE 184 -#define LOAD_ATTR_NONDESCRIPTOR_NO_DICT 185 -#define LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES 186 -#define LOAD_ATTR_PROPERTY 187 -#define LOAD_ATTR_SLOT 188 -#define LOAD_ATTR_WITH_HINT 189 -#define LOAD_CONST_IMMORTAL 190 -#define LOAD_CONST_MORTAL 191 -#define LOAD_GLOBAL_BUILTIN 192 -#define LOAD_GLOBAL_MODULE 193 -#define LOAD_SUPER_ATTR_ATTR 194 -#define LOAD_SUPER_ATTR_METHOD 195 -#define RESUME_CHECK 196 -#define SEND_GEN 197 -#define STORE_ATTR_INSTANCE_VALUE 198 -#define STORE_ATTR_SLOT 199 -#define STORE_ATTR_WITH_HINT 200 -#define STORE_SUBSCR_DICT 201 -#define STORE_SUBSCR_LIST_INT 202 -#define TO_BOOL_ALWAYS_TRUE 203 -#define TO_BOOL_BOOL 204 -#define TO_BOOL_INT 205 -#define TO_BOOL_LIST 206 -#define TO_BOOL_NONE 207 -#define TO_BOOL_STR 208 -#define UNPACK_SEQUENCE_LIST 209 -#define UNPACK_SEQUENCE_TUPLE 210 -#define UNPACK_SEQUENCE_TWO_TUPLE 211 -#define INSTRUMENTED_END_FOR 234 -#define INSTRUMENTED_POP_ITER 235 -#define INSTRUMENTED_END_SEND 236 -#define INSTRUMENTED_FOR_ITER 237 -#define INSTRUMENTED_INSTRUCTION 238 -#define INSTRUMENTED_JUMP_FORWARD 239 -#define INSTRUMENTED_NOT_TAKEN 240 -#define INSTRUMENTED_POP_JUMP_IF_TRUE 241 -#define INSTRUMENTED_POP_JUMP_IF_FALSE 242 -#define INSTRUMENTED_POP_JUMP_IF_NONE 243 -#define INSTRUMENTED_POP_JUMP_IF_NOT_NONE 244 -#define INSTRUMENTED_RESUME 245 -#define INSTRUMENTED_RETURN_VALUE 246 -#define INSTRUMENTED_YIELD_VALUE 247 -#define INSTRUMENTED_END_ASYNC_FOR 248 -#define INSTRUMENTED_LOAD_SUPER_ATTR 249 -#define INSTRUMENTED_CALL 250 -#define INSTRUMENTED_CALL_KW 251 -#define INSTRUMENTED_CALL_FUNCTION_EX 252 -#define INSTRUMENTED_JUMP_BACKWARD 253 -#define INSTRUMENTED_LINE 254 -#define ENTER_EXECUTOR 255 +#define BINARY_OP_SUBSCR_USTR_INT 141 +#define BINARY_OP_SUBTRACT_FLOAT 142 +#define BINARY_OP_SUBTRACT_INT 143 +#define CALL_ALLOC_AND_ENTER_INIT 144 +#define CALL_BOUND_METHOD_EXACT_ARGS 145 +#define CALL_BOUND_METHOD_GENERAL 146 +#define CALL_BUILTIN_CLASS 147 +#define CALL_BUILTIN_FAST 148 +#define CALL_BUILTIN_FAST_WITH_KEYWORDS 149 +#define CALL_BUILTIN_O 150 +#define CALL_EX_NON_PY_GENERAL 151 +#define CALL_EX_PY 152 +#define CALL_ISINSTANCE 153 +#define CALL_KW_BOUND_METHOD 154 +#define CALL_KW_NON_PY 155 +#define CALL_KW_PY 156 +#define CALL_LEN 157 +#define CALL_LIST_APPEND 158 +#define CALL_METHOD_DESCRIPTOR_FAST 159 +#define CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS 160 +#define CALL_METHOD_DESCRIPTOR_NOARGS 161 +#define CALL_METHOD_DESCRIPTOR_O 162 +#define CALL_NON_PY_GENERAL 163 +#define CALL_PY_EXACT_ARGS 164 +#define CALL_PY_GENERAL 165 +#define CALL_STR_1 166 +#define CALL_TUPLE_1 167 +#define CALL_TYPE_1 168 +#define COMPARE_OP_FLOAT 169 +#define COMPARE_OP_INT 170 +#define COMPARE_OP_STR 171 +#define CONTAINS_OP_DICT 172 +#define CONTAINS_OP_SET 173 +#define FOR_ITER_GEN 174 +#define FOR_ITER_LIST 175 +#define FOR_ITER_RANGE 176 +#define FOR_ITER_TUPLE 177 +#define FOR_ITER_VIRTUAL 178 +#define GET_ITER_SELF 179 +#define GET_ITER_VIRTUAL 180 +#define JUMP_BACKWARD_JIT 181 +#define JUMP_BACKWARD_NO_JIT 182 +#define LOAD_ATTR_CLASS 183 +#define LOAD_ATTR_CLASS_WITH_METACLASS_CHECK 184 +#define LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN 185 +#define LOAD_ATTR_INSTANCE_VALUE 186 +#define LOAD_ATTR_METHOD_LAZY_DICT 187 +#define LOAD_ATTR_METHOD_NO_DICT 188 +#define LOAD_ATTR_METHOD_WITH_VALUES 189 +#define LOAD_ATTR_MODULE 190 +#define LOAD_ATTR_NONDESCRIPTOR_NO_DICT 191 +#define LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES 192 +#define LOAD_ATTR_PROPERTY 193 +#define LOAD_ATTR_SLOT 194 +#define LOAD_ATTR_WITH_HINT 195 +#define LOAD_GLOBAL_BUILTIN 196 +#define LOAD_GLOBAL_MODULE 197 +#define LOAD_SUPER_ATTR_ATTR 198 +#define LOAD_SUPER_ATTR_METHOD 199 +#define RESUME_CHECK 200 +#define RESUME_CHECK_JIT 201 +#define SEND_ASYNC_GEN 202 +#define SEND_GEN 203 +#define SEND_VIRTUAL 204 +#define STORE_ATTR_INSTANCE_VALUE 205 +#define STORE_ATTR_SLOT 206 +#define STORE_ATTR_WITH_HINT 207 +#define STORE_SUBSCR_DICT 208 +#define STORE_SUBSCR_LIST_INT 209 +#define TO_BOOL_ALWAYS_TRUE 210 +#define TO_BOOL_BOOL 211 +#define TO_BOOL_INT 212 +#define TO_BOOL_LIST 213 +#define TO_BOOL_NONE 214 +#define TO_BOOL_STR 215 +#define UNPACK_SEQUENCE_LIST 216 +#define UNPACK_SEQUENCE_TUPLE 217 +#define UNPACK_SEQUENCE_TWO_TUPLE 218 +#define INSTRUMENTED_END_FOR 233 +#define INSTRUMENTED_POP_ITER 234 +#define INSTRUMENTED_END_SEND 235 +#define INSTRUMENTED_FOR_ITER 236 +#define INSTRUMENTED_INSTRUCTION 237 +#define INSTRUMENTED_JUMP_FORWARD 238 +#define INSTRUMENTED_NOT_TAKEN 239 +#define INSTRUMENTED_POP_JUMP_IF_TRUE 240 +#define INSTRUMENTED_POP_JUMP_IF_FALSE 241 +#define INSTRUMENTED_POP_JUMP_IF_NONE 242 +#define INSTRUMENTED_POP_JUMP_IF_NOT_NONE 243 +#define INSTRUMENTED_RESUME 244 +#define INSTRUMENTED_RETURN_VALUE 245 +#define INSTRUMENTED_YIELD_VALUE 246 +#define INSTRUMENTED_END_ASYNC_FOR 247 +#define INSTRUMENTED_LOAD_SUPER_ATTR 248 +#define INSTRUMENTED_CALL 249 +#define INSTRUMENTED_CALL_KW 250 +#define INSTRUMENTED_CALL_FUNCTION_EX 251 +#define INSTRUMENTED_JUMP_BACKWARD 252 +#define INSTRUMENTED_LINE 253 +#define ENTER_EXECUTOR 254 +#define TRACE_RECORD 255 #define ANNOTATIONS_PLACEHOLDER 256 #define JUMP 257 #define JUMP_IF_FALSE 258 @@ -249,9 +256,9 @@ extern "C" { #define SETUP_WITH 265 #define STORE_FAST_MAYBE_NULL 266 -#define HAVE_ARGUMENT 43 +#define HAVE_ARGUMENT 41 #define MIN_SPECIALIZED_OPCODE 129 -#define MIN_INSTRUMENTED_OPCODE 234 +#define MIN_INSTRUMENTED_OPCODE 233 #ifdef __cplusplus } diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/patchlevel.h b/python-multiplatform/src/nativeInterop/cinterop/include/patchlevel.h index 1b1e2f825..9e2baa590 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/patchlevel.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/patchlevel.h @@ -6,6 +6,9 @@ configure.ac must also be changed. There is also (independent) API version information in modsupport.h. + + This header should self-contained; PC/python_ver_rc.h includes it + without the rest of Python.h. */ /* Values for PY_RELEASE_LEVEL */ @@ -18,13 +21,13 @@ /* Version parsed out into numeric values */ /*--start constants--*/ #define PY_MAJOR_VERSION 3 -#define PY_MINOR_VERSION 14 -#define PY_MICRO_VERSION 7 +#define PY_MINOR_VERSION 15 +#define PY_MICRO_VERSION 0 #define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL #define PY_RELEASE_SERIAL 0 /* Version as a string */ -#define PY_VERSION "3.14.7" +#define PY_VERSION "3.15.0" /*--end constants--*/ @@ -46,4 +49,16 @@ // Public Py_PACK_VERSION is declared in pymacro.h; it needs . + +/* The API and ABI versions are left for backwards compatibility. + They've not been updated since 2006 and 2010, respectively. + API/ABI versioning is now tied to the CPython version. + The *_VERSION and *_STRING symbols should define the same value; as + number and string literal respectively. Make sure the definitions match. +*/ +#define PYTHON_API_VERSION 1013 +#define PYTHON_API_STRING "1013" +#define PYTHON_ABI_VERSION 3 +#define PYTHON_ABI_STRING "3" + #endif //_Py_PATCHLEVEL_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/py_curses.h b/python-multiplatform/src/nativeInterop/cinterop/include/py_curses.h index 3443ff7d5..1127f1ea2 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/py_curses.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/py_curses.h @@ -116,6 +116,13 @@ static void **PyCurses_API; static const char catchall_ERR[] = "curses function returned ERR"; static const char catchall_NULL[] = "curses function returned NULL"; +#if defined(CURSES_MODULE) || defined(CURSES_PANEL_MODULE) +/* Error messages shared by the curses package */ +# define CURSES_ERROR_FORMAT "%s() returned %s" +# define CURSES_ERROR_VERBOSE_FORMAT "%s() (called by %s()) returned %s" +# define CURSES_ERROR_MUST_CALL_FORMAT "must call %s() first" +#endif + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyabi.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyabi.h new file mode 100644 index 000000000..21a6ab0c1 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pyabi.h @@ -0,0 +1,123 @@ +/* Macros that restrict available definitions and select implementations + * to match an ABI stability promise: + * + * - internal API/ABI (may change at any time) -- Py_BUILD_CORE* + * - general CPython API/ABI (may change in 3.x.0) -- default + * - Stable ABI: abi3, abi3t (long-term stable) -- Py_LIMITED_API, + * Py_TARGET_ABI3T, _Py_OPAQUE_PYOBJECT + * - Free-threading (incompatible with non-free-threading builds) + * -- Py_GIL_DISABLED + */ + +#ifndef _Py_PYABI_H +#define _Py_PYABI_H + +/* Defines to build Python and its standard library: + * + * - Py_BUILD_CORE: Build Python core. Gives access to Python internals; should + * not be used by third-party modules. + * - Py_BUILD_CORE_BUILTIN: Build a Python stdlib module as a built-in module. + * - Py_BUILD_CORE_MODULE: Build a Python stdlib module as a dynamic library. + * + * Py_BUILD_CORE_BUILTIN and Py_BUILD_CORE_MODULE imply Py_BUILD_CORE. + * + * On Windows, Py_BUILD_CORE_MODULE exports "PyInit_xxx" symbol, whereas + * Py_BUILD_CORE_BUILTIN does not. + */ +#if defined(Py_BUILD_CORE_BUILTIN) && !defined(Py_BUILD_CORE) +# define Py_BUILD_CORE +#endif +#if defined(Py_BUILD_CORE_MODULE) && !defined(Py_BUILD_CORE) +# define Py_BUILD_CORE +#endif + +/* Check valid values for target ABI macros. + */ +#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 < 3 + // Empty Py_LIMITED_API used to work; redefine to + // Python 3.2 to be explicit. +# undef Py_LIMITED_API +# define Py_LIMITED_API 0x03020000 +#endif +#if defined(Py_TARGET_ABI3T) && Py_TARGET_ABI3T+0 < 0x030f0000 +# error "Py_TARGET_ABI3T must be 0x030f0000 (3.15) or above" +#endif + +/* Stable ABI for free-threaded builds (abi3t, introduced in PEP 803) + * is enabled by one of: + * - Py_TARGET_ABI3T, or + * - Py_LIMITED_API and Py_GIL_DISABLED. + * + * These affect set the following, which Python.h should use internally: + * - Py_LIMITED_API (defines the subset of API we expose) + * - _Py_OPAQUE_PYOBJECT (additionally hides what's ABI-incompatible between + * free-threaded & GIL) + * + * (Don't use Py_TARGET_ABI3T directly. It's currently only used to set these + * 2 macros, and defined for users' convenience.) + * + * This logic is currently partially duplicated in PC/pyconfig.h. + */ +#if defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED) \ + && !defined(Py_TARGET_ABI3T) +# define Py_TARGET_ABI3T Py_LIMITED_API +#endif +#if defined(Py_TARGET_ABI3T) +# define _Py_OPAQUE_PYOBJECT +# if !defined(Py_LIMITED_API) +# define Py_LIMITED_API Py_TARGET_ABI3T +# elif Py_LIMITED_API > Py_TARGET_ABI3T + // if both are defined, use the *lower* version, + // i.e. maximum compatibility +# undef Py_LIMITED_API +# define Py_LIMITED_API Py_TARGET_ABI3T +# endif +#else +# ifdef _Py_OPAQUE_PYOBJECT + // _Py_OPAQUE_PYOBJECT is a private macro; do not define it directly. +# error "Define Py_TARGET_ABI3T to target abi3t." +# endif +#endif + +#if defined(Py_TARGET_ABI3T) +# if !defined(Py_GIL_DISABLED) + // Define Py_GIL_DISABLED for users' needs. Users check this macro to see + // whether they need extra synchronization. +# define Py_GIL_DISABLED +# endif +# if defined(_Py_IS_TESTCEXT) + // When compiling for abi3t, contents of Python.h should not depend + // on Py_GIL_DISABLED. + // We ask GCC to error if it sees the macro from this point on. + // Since users are free to the macro, and there's no way to undo the + // poisoning at the end of Python.h, we only do this in a test module + // (test_cext). + // + // Clang's poisoning is stricter than GCC's: it looks in `#elif` + // expressions after matching `#if`s. We disable it for now. + // We also provide an undocumented, unsupported opt-out macro to help + // porting to other compilers. Consider reaching out if you use it. +# if defined(__GNUC__) && !defined(__clang__) && !defined(_Py_NO_GCC_POISON) +# undef Py_GIL_DISABLED +# pragma GCC poison Py_GIL_DISABLED +# endif +# endif +#endif + +/* The internal C API must not be used with the limited C API: make sure + * that Py_BUILD_CORE* macros are not defined in this case. + * But, keep the "original" values, under different names, for "exports.h" + */ +#ifdef Py_BUILD_CORE +# define _PyEXPORTS_CORE +#endif +#ifdef Py_BUILD_CORE_MODULE +# define _PyEXPORTS_CORE_MODULE +#endif +#ifdef Py_LIMITED_API +# undef Py_BUILD_CORE +# undef Py_BUILD_CORE_BUILTIN +# undef Py_BUILD_CORE_MODULE +#endif + +#endif // _Py_PYABI_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pybuffer.h b/python-multiplatform/src/nativeInterop/cinterop/include/pybuffer.h index ca1c6058d..6371b9b48 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pybuffer.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pybuffer.h @@ -67,27 +67,27 @@ PyAPI_FUNC(int) PyBuffer_FromContiguous(const Py_buffer *view, const void *buf, error (i.e. the object does not have a buffer interface or it is not working). - If fort is 'F', then if the object is multi-dimensional, + If order is 'F', then if the object is multi-dimensional, then the data will be copied into the array in Fortran-style (first dimension varies the fastest). If - fort is 'C', then the data will be copied into the array - in C-style (last dimension varies the fastest). If fort + order is 'C', then the data will be copied into the array + in C-style (last dimension varies the fastest). If order is 'A', then it does not matter and the copy will be made in whatever way is more efficient. */ PyAPI_FUNC(int) PyObject_CopyData(PyObject *dest, PyObject *src); /* Copy the data from the src buffer to the buffer of destination. */ -PyAPI_FUNC(int) PyBuffer_IsContiguous(const Py_buffer *view, char fort); +PyAPI_FUNC(int) PyBuffer_IsContiguous(const Py_buffer *view, char order); /*Fill the strides array with byte-strides of a contiguous - (Fortran-style if fort is 'F' or C-style otherwise) + (Fortran-style if order is 'F' or C-style otherwise) array of the given shape with the given number of bytes per element. */ PyAPI_FUNC(void) PyBuffer_FillContiguousStrides(int ndims, Py_ssize_t *shape, Py_ssize_t *strides, int itemsize, - char fort); + char order); /* Fills in a buffer-info structure correctly for an exporter that can only share a contiguous chunk of memory of diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-android-aarch64.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-android-aarch64.h deleted file mode 100644 index 26a025247..000000000 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-android-aarch64.h +++ /dev/null @@ -1,2115 +0,0 @@ -/* pyconfig.h. Generated from pyconfig.h.in by configure. */ -/* pyconfig.h.in. Generated from configure.ac by autoheader. */ - - -#ifndef Py_PYCONFIG_H -#define Py_PYCONFIG_H - - -/* Define if building universal (internal helper macro) */ -/* #undef AC_APPLE_UNIVERSAL_BUILD */ - -/* BUILD_GNU_TYPE + AIX_BUILDDATE are used to construct the PEP425 tag of the - build system. */ -/* #undef AIX_BUILDDATE */ - -/* Define for AIX if your compiler is a genuine IBM xlC/xlC_r and you want - support for AIX C++ shared extension modules. */ -/* #undef AIX_GENUINE_CPLUSPLUS */ - -/* The normal alignment of 'long', in bytes. */ -#define ALIGNOF_LONG 8 - -/* The normal alignment of 'max_align_t', in bytes. */ -#define ALIGNOF_MAX_ALIGN_T 16 - -/* The normal alignment of 'size_t', in bytes. */ -#define ALIGNOF_SIZE_T 8 - -/* Alternative SOABI used in debug build to load C extensions built in release - mode */ -/* #undef ALT_SOABI */ - -/* The Android API level. */ -#define ANDROID_API_LEVEL 24 - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored in ARM - mixed-endian order (byte order 45670123) */ -/* #undef DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the most - significant byte first */ -/* #undef DOUBLE_IS_BIG_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the - least significant byte first */ -#define DOUBLE_IS_LITTLE_ENDIAN_IEEE754 1 - -/* Define if --enable-ipv6 is specified */ -#define ENABLE_IPV6 1 - -/* Define if getpgrp() must be called as getpgrp(0). */ -/* #undef GETPGRP_HAVE_ARG */ - -/* Define if you have the 'accept' function. */ -#define HAVE_ACCEPT 1 - -/* Define to 1 if you have the 'accept4' function. */ -#define HAVE_ACCEPT4 1 - -/* Define to 1 if you have the 'acosh' function. */ -#define HAVE_ACOSH 1 - -/* struct addrinfo (netdb.h) */ -#define HAVE_ADDRINFO 1 - -/* Define to 1 if you have the 'alarm' function. */ -#define HAVE_ALARM 1 - -/* Define if aligned memory access is required */ -/* #undef HAVE_ALIGNED_REQUIRED */ - -/* Define to 1 if you have the header file. */ -#define HAVE_ALLOCA_H 1 - -/* Define this if your time.h defines altzone. */ -/* #undef HAVE_ALTZONE */ - -/* Define to 1 if you have the 'asinh' function. */ -#define HAVE_ASINH 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ASM_TYPES_H 1 - -/* Define to 1 if you have the 'atanh' function. */ -#define HAVE_ATANH 1 - -/* Define to 1 if you have the 'backtrace' function. */ -/* #undef HAVE_BACKTRACE */ - -/* Define if you have the 'bind' function. */ -#define HAVE_BIND 1 - -/* Define to 1 if you have the 'bind_textdomain_codeset' function. */ -/* #undef HAVE_BIND_TEXTDOMAIN_CODESET */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_BLUETOOTH_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_H */ - -/* Define if mbstowcs(NULL, "text", 0) does not return the number of wide - chars that would be converted. */ -/* #undef HAVE_BROKEN_MBSTOWCS */ - -/* Define if nice() returns success/failure instead of the new priority. */ -/* #undef HAVE_BROKEN_NICE */ - -/* Define if the system reports an invalid PIPE_BUF value. */ -/* #undef HAVE_BROKEN_PIPE_BUF */ - -/* Define if poll() sets errno on invalid file descriptors. */ -/* #undef HAVE_BROKEN_POLL */ - -/* Define if the Posix semaphores do not work on your system */ -/* #undef HAVE_BROKEN_POSIX_SEMAPHORES */ - -/* Define if pthread_sigmask() does not work on your system. */ -/* #undef HAVE_BROKEN_PTHREAD_SIGMASK */ - -/* define to 1 if your sem_getvalue is broken. */ -#define HAVE_BROKEN_SEM_GETVALUE 1 - -/* Define if 'unsetenv' does not return an int. */ -/* #undef HAVE_BROKEN_UNSETENV */ - -/* Has builtin __atomic_load_n() and __atomic_store_n() functions */ -#define HAVE_BUILTIN_ATOMIC 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_BZLIB_H 1 - -/* Define to 1 if you have the 'chflags' function. */ -/* #undef HAVE_CHFLAGS */ - -/* Define to 1 if you have the 'chmod' function. */ -#define HAVE_CHMOD 1 - -/* Define to 1 if you have the 'chown' function. */ -#define HAVE_CHOWN 1 - -/* Define if you have the 'chroot' function. */ -/* #undef HAVE_CHROOT */ - -/* Define to 1 if you have the 'clock' function. */ -#define HAVE_CLOCK 1 - -/* Define to 1 if you have the 'clock_getres' function. */ -#define HAVE_CLOCK_GETRES 1 - -/* Define to 1 if you have the 'clock_gettime' function. */ -#define HAVE_CLOCK_GETTIME 1 - -/* Define to 1 if you have the 'clock_nanosleep' function. */ -#define HAVE_CLOCK_NANOSLEEP 1 - -/* Define to 1 if you have the 'clock_settime' function. */ -/* #undef HAVE_CLOCK_SETTIME */ - -/* Define to 1 if the system has the type 'clock_t'. */ -#define HAVE_CLOCK_T 1 - -/* Define to 1 if you have the 'closefrom' function. */ -/* #undef HAVE_CLOSEFROM */ - -/* Define to 1 if you have the 'close_range' function. */ -/* #undef HAVE_CLOSE_RANGE */ - -/* Define if the C compiler supports computed gotos. */ -/* #undef HAVE_COMPUTED_GOTOS */ - -/* Define to 1 if you have the 'confstr' function. */ -/* #undef HAVE_CONFSTR */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CONIO_H */ - -/* Define if you have the 'connect' function. */ -#define HAVE_CONNECT 1 - -/* Define to 1 if you have the 'copy_file_range' function. */ -/* #undef HAVE_COPY_FILE_RANGE */ - -/* Define to 1 if you have the 'ctermid' function. */ -/* #undef HAVE_CTERMID */ - -/* Define if you have the 'ctermid_r' function. */ -/* #undef HAVE_CTERMID_R */ - -/* Define if you have the 'ESCDELAY' variable. */ -/* #undef HAVE_CURSES_ESCDELAY */ - -/* Define if you have the 'filter' function. */ -/* #undef HAVE_CURSES_FILTER */ - -/* Define if you have the 'getmouse' function with the X/Open signature. */ -/* #undef HAVE_CURSES_GETMOUSE */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CURSES_H */ - -/* Define if you have the 'has_key' function. */ -/* #undef HAVE_CURSES_HAS_KEY */ - -/* Define if you have the 'immedok' function. */ -/* #undef HAVE_CURSES_IMMEDOK */ - -/* Define if you have the 'is_pad' function. */ -/* #undef HAVE_CURSES_IS_PAD */ - -/* Define if you have the 'is_term_resized' function. */ -/* #undef HAVE_CURSES_IS_TERM_RESIZED */ - -/* Define if you have the 'resizeterm' function. */ -/* #undef HAVE_CURSES_RESIZETERM */ - -/* Define if you have the 'resize_term' function. */ -/* #undef HAVE_CURSES_RESIZE_TERM */ - -/* Define if you have the 'set_escdelay' function. */ -/* #undef HAVE_CURSES_SET_ESCDELAY */ - -/* Define if you have the 'set_tabsize' function. */ -/* #undef HAVE_CURSES_SET_TABSIZE */ - -/* Define if you have the 'syncok' function. */ -/* #undef HAVE_CURSES_SYNCOK */ - -/* Define if you have the 'TABSIZE' variable. */ -/* #undef HAVE_CURSES_TABSIZE */ - -/* Define if you have the 'typeahead' function. */ -/* #undef HAVE_CURSES_TYPEAHEAD */ - -/* Define if you have the 'use_env' function. */ -/* #undef HAVE_CURSES_USE_ENV */ - -/* Define if you have the 'wchgat' function. */ -/* #undef HAVE_CURSES_WCHGAT */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_DB_H */ - -/* Define to 1 if you have the declaration of 'RTLD_DEEPBIND', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_DEEPBIND 0 - -/* Define to 1 if you have the declaration of 'RTLD_GLOBAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_GLOBAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_LAZY', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LAZY 1 - -/* Define to 1 if you have the declaration of 'RTLD_LOCAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LOCAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_MEMBER', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_MEMBER 0 - -/* Define to 1 if you have the declaration of 'RTLD_NODELETE', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NODELETE 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOLOAD', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOLOAD 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOW', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOW 1 - -/* Define to 1 if you have the declaration of 'tzname', and to 0 if you don't. - */ -/* #undef HAVE_DECL_TZNAME */ - -/* Define to 1 if you have the declaration of 'UT_NAMESIZE', and to 0 if you - don't. */ -#define HAVE_DECL_UT_NAMESIZE 1 - -/* Define to 1 if you have the device macros. */ -#define HAVE_DEVICE_MACROS 1 - -/* Define to 1 if you have the /dev/ptc device file. */ -/* #undef HAVE_DEV_PTC */ - -/* Define to 1 if you have the /dev/ptmx device file. */ -/* #undef HAVE_DEV_PTMX */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_DIRECT_H */ - -/* Define to 1 if the dirent structure has a d_type field */ -#define HAVE_DIRENT_D_TYPE 1 - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -#define HAVE_DIRENT_H 1 - -/* Define if you have the 'dirfd' function or macro. */ -#define HAVE_DIRFD 1 - -/* Define to 1 if you have the 'dladdr' function. */ -#define HAVE_DLADDR 1 - -/* Define to 1 if you have the 'dladdr1' function. */ -/* #undef HAVE_DLADDR1 */ - -/* Define to 1 if you have the header file. */ -#define HAVE_DLFCN_H 1 - -/* Define to 1 if you have the 'dlopen' function. */ -#define HAVE_DLOPEN 1 - -/* Define to 1 if you have the 'dl_iterate_phdr' function. */ -#define HAVE_DL_ITERATE_PHDR 1 - -/* Define to 1 if you have the 'dup' function. */ -#define HAVE_DUP 1 - -/* Define to 1 if you have the 'dup2' function. */ -#define HAVE_DUP2 1 - -/* Define to 1 if you have the 'dup3' function. */ -#define HAVE_DUP3 1 - -/* Define if you have the '_dyld_shared_cache_contains_path' function. */ -/* #undef HAVE_DYLD_SHARED_CACHE_CONTAINS_PATH */ - -/* Defined when any dynamic module loading is enabled. */ -#define HAVE_DYNAMIC_LOADING 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_EDITLINE_READLINE_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_ENDIAN_H 1 - -/* Define if you have the 'epoll_create' function. */ -#define HAVE_EPOLL 1 - -/* Define if you have the 'epoll_create1' function. */ -#define HAVE_EPOLL_CREATE1 1 - -/* Define to 1 if you have the 'erf' function. */ -#define HAVE_ERF 1 - -/* Define to 1 if you have the 'erfc' function. */ -#define HAVE_ERFC 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ERRNO_H 1 - -/* Define if you have the 'eventfd' function. */ -#define HAVE_EVENTFD 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_EXECINFO_H 1 - -/* Define to 1 if you have the 'execv' function. */ -#define HAVE_EXECV 1 - -/* Define to 1 if you have the 'explicit_bzero' function. */ -/* #undef HAVE_EXPLICIT_BZERO */ - -/* Define to 1 if you have the 'explicit_memset' function. */ -/* #undef HAVE_EXPLICIT_MEMSET */ - -/* Define to 1 if you have the 'expm1' function. */ -#define HAVE_EXPM1 1 - -/* Define to 1 if you have the 'faccessat' function. */ -#define HAVE_FACCESSAT 1 - -/* Define if you have the 'fchdir' function. */ -#define HAVE_FCHDIR 1 - -/* Define to 1 if you have the 'fchmod' function. */ -#define HAVE_FCHMOD 1 - -/* Define to 1 if you have the 'fchmodat' function. */ -#define HAVE_FCHMODAT 1 - -/* Define to 1 if you have the 'fchown' function. */ -#define HAVE_FCHOWN 1 - -/* Define to 1 if you have the 'fchownat' function. */ -#define HAVE_FCHOWNAT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_FCNTL_H 1 - -/* Define if you have the 'fdatasync' function. */ -#define HAVE_FDATASYNC 1 - -/* Define to 1 if you have the 'fdopendir' function. */ -#define HAVE_FDOPENDIR 1 - -/* Define to 1 if you have the 'fdwalk' function. */ -/* #undef HAVE_FDWALK */ - -/* Define to 1 if you have the 'fexecve' function. */ -/* #undef HAVE_FEXECVE */ - -/* Define if you have the 'ffi_closure_alloc' function. */ -#define HAVE_FFI_CLOSURE_ALLOC 1 - -/* Define if you have the 'ffi_prep_cif_var' function. */ -#define HAVE_FFI_PREP_CIF_VAR 1 - -/* Define if you have the 'ffi_prep_closure_loc' function. */ -#define HAVE_FFI_PREP_CLOSURE_LOC 1 - -/* Define to 1 if you have the 'flock' function. */ -#define HAVE_FLOCK 1 - -/* Define to 1 if you have the 'fork' function. */ -#define HAVE_FORK 1 - -/* Define to 1 if you have the 'fork1' function. */ -/* #undef HAVE_FORK1 */ - -/* Define to 1 if you have the 'forkpty' function. */ -#define HAVE_FORKPTY 1 - -/* Define to 1 if you have the 'fpathconf' function. */ -#define HAVE_FPATHCONF 1 - -/* Define to 1 if you have the 'fseek64' function. */ -/* #undef HAVE_FSEEK64 */ - -/* Define to 1 if you have the 'fseeko' function. */ -#define HAVE_FSEEKO 1 - -/* Define to 1 if you have the 'fstatat' function. */ -#define HAVE_FSTATAT 1 - -/* Define to 1 if you have the 'fstatvfs' function. */ -#define HAVE_FSTATVFS 1 - -/* Define if you have the 'fsync' function. */ -#define HAVE_FSYNC 1 - -/* Define to 1 if you have the 'ftell64' function. */ -/* #undef HAVE_FTELL64 */ - -/* Define to 1 if you have the 'ftello' function. */ -#define HAVE_FTELLO 1 - -/* Define to 1 if you have the 'ftime' function. */ -/* #undef HAVE_FTIME */ - -/* Define to 1 if you have the 'ftruncate' function. */ -#define HAVE_FTRUNCATE 1 - -/* Define to 1 if you have the 'futimens' function. */ -#define HAVE_FUTIMENS 1 - -/* Define to 1 if you have the 'futimes' function. */ -/* #undef HAVE_FUTIMES */ - -/* Define to 1 if you have the 'futimesat' function. */ -/* #undef HAVE_FUTIMESAT */ - -/* Define to 1 if you have the 'gai_strerror' function. */ -#define HAVE_GAI_STRERROR 1 - -/* Define if we can use gcc inline assembler to get and set mc68881 fpcr */ -/* #undef HAVE_GCC_ASM_FOR_MC68881 */ - -/* Define if we can use x64 gcc inline assembler */ -/* #undef HAVE_GCC_ASM_FOR_X64 */ - -/* Define if we can use gcc inline assembler to get and set x87 control word - */ -/* #undef HAVE_GCC_ASM_FOR_X87 */ - -/* Define if your compiler provides __uint128_t */ -#define HAVE_GCC_UINT128_T 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_DASH_NDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_NDBM_H */ - -/* Define if you have the getaddrinfo function. */ -#define HAVE_GETADDRINFO 1 - -/* Define this if you have flockfile(), getc_unlocked(), and funlockfile() */ -#define HAVE_GETC_UNLOCKED 1 - -/* Define to 1 if you have the 'getegid' function. */ -#define HAVE_GETEGID 1 - -/* Define to 1 if you have the 'getentropy' function. */ -/* #undef HAVE_GETENTROPY */ - -/* Define to 1 if you have the 'geteuid' function. */ -#define HAVE_GETEUID 1 - -/* Define to 1 if you have the 'getgid' function. */ -#define HAVE_GETGID 1 - -/* Define to 1 if you have the 'getgrent' function. */ -/* #undef HAVE_GETGRENT */ - -/* Define to 1 if you have the 'getgrgid' function. */ -#define HAVE_GETGRGID 1 - -/* Define to 1 if you have the 'getgrgid_r' function. */ -#define HAVE_GETGRGID_R 1 - -/* Define to 1 if you have the 'getgrnam_r' function. */ -#define HAVE_GETGRNAM_R 1 - -/* Define to 1 if you have the 'getgrouplist' function. */ -#define HAVE_GETGROUPLIST 1 - -/* Define to 1 if you have the 'getgroups' function. */ -#define HAVE_GETGROUPS 1 - -/* Define if you have the 'gethostbyaddr' function. */ -#define HAVE_GETHOSTBYADDR 1 - -/* Define to 1 if you have the 'gethostbyname' function. */ -#define HAVE_GETHOSTBYNAME 1 - -/* Define this if you have some version of gethostbyname_r() */ -#define HAVE_GETHOSTBYNAME_R 1 - -/* Define this if you have the 3-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_3_ARG */ - -/* Define this if you have the 5-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_5_ARG */ - -/* Define this if you have the 6-arg version of gethostbyname_r(). */ -#define HAVE_GETHOSTBYNAME_R_6_ARG 1 - -/* Define to 1 if you have the 'gethostname' function. */ -#define HAVE_GETHOSTNAME 1 - -/* Define to 1 if you have the 'getitimer' function. */ -#define HAVE_GETITIMER 1 - -/* Define to 1 if you have the 'getloadavg' function. */ -/* #undef HAVE_GETLOADAVG */ - -/* Define to 1 if you have the 'getlogin' function. */ -#define HAVE_GETLOGIN 1 - -/* Define to 1 if you have the 'getlogin_r' function. */ -/* #undef HAVE_GETLOGIN_R */ - -/* Define to 1 if you have the 'getnameinfo' function. */ -#define HAVE_GETNAMEINFO 1 - -/* Define if you have the 'getpagesize' function. */ -#define HAVE_GETPAGESIZE 1 - -/* Define if you have the 'getpeername' function. */ -#define HAVE_GETPEERNAME 1 - -/* Define to 1 if you have the 'getpgid' function. */ -#define HAVE_GETPGID 1 - -/* Define to 1 if you have the 'getpgrp' function. */ -#define HAVE_GETPGRP 1 - -/* Define to 1 if you have the 'getpid' function. */ -#define HAVE_GETPID 1 - -/* Define to 1 if you have the 'getppid' function. */ -#define HAVE_GETPPID 1 - -/* Define to 1 if you have the 'getpriority' function. */ -#define HAVE_GETPRIORITY 1 - -/* Define if you have the 'getprotobyname' function. */ -#define HAVE_GETPROTOBYNAME 1 - -/* Define to 1 if you have the 'getpwent' function. */ -/* #undef HAVE_GETPWENT */ - -/* Define to 1 if you have the 'getpwnam_r' function. */ -#define HAVE_GETPWNAM_R 1 - -/* Define to 1 if you have the 'getpwuid' function. */ -#define HAVE_GETPWUID 1 - -/* Define to 1 if you have the 'getpwuid_r' function. */ -#define HAVE_GETPWUID_R 1 - -/* Define to 1 if the getrandom() function is available */ -/* #undef HAVE_GETRANDOM */ - -/* Define to 1 if the Linux getrandom() syscall is available */ -#define HAVE_GETRANDOM_SYSCALL 1 - -/* Define to 1 if you have the 'getresgid' function. */ -#define HAVE_GETRESGID 1 - -/* Define to 1 if you have the 'getresuid' function. */ -#define HAVE_GETRESUID 1 - -/* Define to 1 if you have the 'getrusage' function. */ -#define HAVE_GETRUSAGE 1 - -/* Define if you have the 'getservbyname' function. */ -#define HAVE_GETSERVBYNAME 1 - -/* Define if you have the 'getservbyport' function. */ -#define HAVE_GETSERVBYPORT 1 - -/* Define to 1 if you have the 'getsid' function. */ -#define HAVE_GETSID 1 - -/* Define if you have the 'getsockname' function. */ -#define HAVE_GETSOCKNAME 1 - -/* Define to 1 if you have the 'getspent' function. */ -/* #undef HAVE_GETSPENT */ - -/* Define to 1 if you have the 'getspnam' function. */ -/* #undef HAVE_GETSPNAM */ - -/* Define to 1 if you have the 'getuid' function. */ -#define HAVE_GETUID 1 - -/* Define to 1 if you have the 'getwd' function. */ -/* #undef HAVE_GETWD */ - -/* Define if glibc has incorrect _FORTIFY_SOURCE wrappers for memmove and - bcopy. */ -/* #undef HAVE_GLIBC_MEMMOVE_BUG */ - -/* Define to 1 if you have the 'grantpt' function. */ -#define HAVE_GRANTPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_GRP_H 1 - -/* Define if you have the 'hstrerror' function. */ -#define HAVE_HSTRERROR 1 - -/* Define this if you have le64toh() */ -#define HAVE_HTOLE64 1 - -/* Define to 1 if you have the 'if_nameindex' function. */ -#define HAVE_IF_NAMEINDEX 1 - -/* Define if you have the 'inet_aton' function. */ -#define HAVE_INET_ATON 1 - -/* Define if you have the 'inet_ntoa' function. */ -#define HAVE_INET_NTOA 1 - -/* Define if you have the 'inet_pton' function. */ -#define HAVE_INET_PTON 1 - -/* Define to 1 if you have the 'initgroups' function. */ -/* #undef HAVE_INITGROUPS */ - -/* Define to 1 if you have the header file. */ -#define HAVE_INTTYPES_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_IO_H */ - -/* Define if gcc has the ipa-pure-const bug. */ -/* #undef HAVE_IPA_PURE_CONST_BUG */ - -/* Define to 1 if you have the 'kill' function. */ -#define HAVE_KILL 1 - -/* Define to 1 if you have the 'killpg' function. */ -#define HAVE_KILLPG 1 - -/* Define if you have the 'kqueue' function. */ -/* #undef HAVE_KQUEUE */ - -/* Define to 1 if you have the header file. */ -#define HAVE_LANGINFO_H 1 - -/* Defined to enable large file support when an off_t is bigger than a long - and long long is at least as big as an off_t. You may need to add some - flags for configuration and compilation to enable this mode. (For Solaris - and Linux, the necessary defines are already defined.) */ -/* #undef HAVE_LARGEFILE_SUPPORT */ - -/* Define to 1 if you have the 'lchflags' function. */ -/* #undef HAVE_LCHFLAGS */ - -/* Define to 1 if you have the 'lchmod' function. */ -/* #undef HAVE_LCHMOD */ - -/* Define to 1 if you have the 'lchown' function. */ -#define HAVE_LCHOWN 1 - -/* Define to 1 if you have the `db' library (-ldb). */ -/* #undef HAVE_LIBDB */ - -/* Define to 1 if you have the 'dl' library (-ldl). */ -#define HAVE_LIBDL 1 - -/* Define to 1 if you have the 'dld' library (-ldld). */ -/* #undef HAVE_LIBDLD */ - -/* Define to 1 if you have the 'ieee' library (-lieee). */ -/* #undef HAVE_LIBIEEE */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBINTL_H */ - -/* Define to 1 if you have the 'sendfile' library (-lsendfile). */ -/* #undef HAVE_LIBSENDFILE */ - -/* Define to 1 if you have the 'sqlite3' library (-lsqlite3). */ -#define HAVE_LIBSQLITE3 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBUTIL_H */ - -/* Define if you have the 'link' function. */ -#define HAVE_LINK 1 - -/* Define to 1 if you have the 'linkat' function. */ -#define HAVE_LINKAT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINK_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_AUXVEC_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_BCM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_J1939_H 1 - -/* Define if compiling using Linux 3.6 or later. */ -#define HAVE_LINUX_CAN_RAW_FD_FRAMES 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_RAW_H 1 - -/* Define if compiling using Linux 4.1 or later. */ -#define HAVE_LINUX_CAN_RAW_JOIN_FILTERS 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_FS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_LIMITS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_MEMFD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_NETFILTER_IPV4_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_NETLINK_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_QRTR_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_RANDOM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_SCHED_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_SOUNDCARD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_TIPC_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_VM_SOCKETS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_WAIT_H 1 - -/* Define if you have the 'listen' function. */ -#define HAVE_LISTEN 1 - -/* Define to 1 if you have the 'lockf' function. */ -#define HAVE_LOCKF 1 - -/* Define to 1 if you have the 'log1p' function. */ -#define HAVE_LOG1P 1 - -/* Define to 1 if you have the 'log2' function. */ -#define HAVE_LOG2 1 - -/* Define to 1 if you have the `login_tty' function. */ -#define HAVE_LOGIN_TTY 1 - -/* Define to 1 if the system has the type 'long double'. */ -#define HAVE_LONG_DOUBLE 1 - -/* Define to 1 if you have the 'lstat' function. */ -#define HAVE_LSTAT 1 - -/* Define to 1 if you have the 'lutimes' function. */ -/* #undef HAVE_LUTIMES */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LZMA_H */ - -/* Define to 1 if you have the 'madvise' function. */ -#define HAVE_MADVISE 1 - -/* Define this if you have the makedev macro. */ -#define HAVE_MAKEDEV 1 - -/* Define if you have the 'MAXLOGNAME' constant. */ -/* #undef HAVE_MAXLOGNAME */ - -/* Define to 1 if you have the 'mbrtowc' function. */ -#define HAVE_MBRTOWC 1 - -/* Define if you have the 'memfd_create' function. */ -/* #undef HAVE_MEMFD_CREATE */ - -/* Define to 1 if you have the 'memrchr' function. */ -#define HAVE_MEMRCHR 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_MINIX_CONFIG_H */ - -/* Define to 1 if you have the 'mkdirat' function. */ -#define HAVE_MKDIRAT 1 - -/* Define to 1 if you have the 'mkfifo' function. */ -#define HAVE_MKFIFO 1 - -/* Define to 1 if you have the 'mkfifoat' function. */ -#define HAVE_MKFIFOAT 1 - -/* Define to 1 if you have the 'mknod' function. */ -#define HAVE_MKNOD 1 - -/* Define to 1 if you have the 'mknodat' function. */ -#define HAVE_MKNODAT 1 - -/* Define to 1 if you have the 'mktime' function. */ -#define HAVE_MKTIME 1 - -/* Define to 1 if you have the 'mmap' function. */ -#define HAVE_MMAP 1 - -/* Define to 1 if you have the 'mremap' function. */ -#define HAVE_MREMAP 1 - -/* Define to 1 if you have the 'nanosleep' function. */ -#define HAVE_NANOSLEEP 1 - -/* Define if you have the 'ncurses' library */ -/* #undef HAVE_NCURSES */ - -/* Define if you have the 'ncursesw' library */ -/* #undef HAVE_NCURSESW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_PANEL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_PANEL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NDBM_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. */ -/* #undef HAVE_NDIR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETCAN_CAN_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NETDB_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NETINET_IN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETLINK_NETLINK_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NETPACKET_PACKET_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_ETHERNET_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_IF_H 1 - -/* Define to 1 if you have the 'nice' function. */ -#define HAVE_NICE 1 - -/* Define if the internal form of wchar_t in non-Unicode locales is not - Unicode. */ -/* #undef HAVE_NON_UNICODE_WCHAR_T_REPRESENTATION */ - -/* Define to 1 if you have the 'openat' function. */ -#define HAVE_OPENAT 1 - -/* Define to 1 if you have the 'opendir' function. */ -#define HAVE_OPENDIR 1 - -/* Define to 1 if you have the 'openpty' function. */ -#define HAVE_OPENPTY 1 - -/* Define if you have the 'panel' library */ -/* #undef HAVE_PANEL */ - -/* Define if you have the 'panelw' library */ -/* #undef HAVE_PANELW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PANEL_H */ - -/* Define to 1 if you have the 'pathconf' function. */ -#define HAVE_PATHCONF 1 - -/* Define to 1 if you have the 'pause' function. */ -#define HAVE_PAUSE 1 - -/* Define to 1 if you have the 'pipe' function. */ -#define HAVE_PIPE 1 - -/* Define to 1 if you have the 'pipe2' function. */ -#define HAVE_PIPE2 1 - -/* Define to 1 if you have the 'plock' function. */ -/* #undef HAVE_PLOCK */ - -/* Define to 1 if you have the 'poll' function. */ -#define HAVE_POLL 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_POLL_H 1 - -/* Define to 1 if you have the 'posix_fadvise' function. */ -#define HAVE_POSIX_FADVISE 1 - -/* Define to 1 if you have the 'posix_fallocate' function. */ -#define HAVE_POSIX_FALLOCATE 1 - -/* Define to 1 if you have the 'posix_openpt' function. */ -#define HAVE_POSIX_OPENPT 1 - -/* Define to 1 if you have the 'posix_spawn' function. */ -/* #undef HAVE_POSIX_SPAWN */ - -/* Define to 1 if you have the 'posix_spawnp' function. */ -/* #undef HAVE_POSIX_SPAWNP */ - -/* Define to 1 if you have the 'posix_spawn_file_actions_addclosefrom_np' - function. */ -/* #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP */ - -/* Define to 1 if you have the 'pread' function. */ -#define HAVE_PREAD 1 - -/* Define to 1 if you have the 'preadv' function. */ -#define HAVE_PREADV 1 - -/* Define to 1 if you have the 'preadv2' function. */ -/* #undef HAVE_PREADV2 */ - -/* Define if you have the 'prlimit' function. */ -#define HAVE_PRLIMIT 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PROCESS_H */ - -/* Define to 1 if you have the 'process_vm_readv' function. */ -#define HAVE_PROCESS_VM_READV 1 - -/* Define if your compiler supports function prototype */ -#define HAVE_PROTOTYPES 1 - -/* Define to 1 if you have the 'pthread_condattr_setclock' function. */ -#define HAVE_PTHREAD_CONDATTR_SETCLOCK 1 - -/* Define to 1 if you have the 'pthread_cond_timedwait_relative_np' function. - */ -/* #undef HAVE_PTHREAD_COND_TIMEDWAIT_RELATIVE_NP */ - -/* Defined for Solaris 2.6 bug in pthread header. */ -/* #undef HAVE_PTHREAD_DESTRUCTOR */ - -/* Define to 1 if you have the 'pthread_getattr_np' function. */ -#define HAVE_PTHREAD_GETATTR_NP 1 - -/* Define to 1 if you have the 'pthread_getcpuclockid' function. */ -#define HAVE_PTHREAD_GETCPUCLOCKID 1 - -/* Define to 1 if you have the 'pthread_getname_np' function. */ -/* #undef HAVE_PTHREAD_GETNAME_NP */ - -/* Define to 1 if you have the 'pthread_get_name_np' function. */ -/* #undef HAVE_PTHREAD_GET_NAME_NP */ - -/* Define to 1 if you have the header file. */ -#define HAVE_PTHREAD_H 1 - -/* Define to 1 if you have the 'pthread_init' function. */ -/* #undef HAVE_PTHREAD_INIT */ - -/* Define to 1 if you have the 'pthread_kill' function. */ -#define HAVE_PTHREAD_KILL 1 - -/* Define to 1 if you have the 'pthread_setname_np' function. */ -#define HAVE_PTHREAD_SETNAME_NP 1 - -/* Define to 1 if you have the 'pthread_set_name_np' function. */ -/* #undef HAVE_PTHREAD_SET_NAME_NP */ - -/* Define to 1 if you have the 'pthread_sigmask' function. */ -#define HAVE_PTHREAD_SIGMASK 1 - -/* Define if platform requires stubbed pthreads support */ -/* #undef HAVE_PTHREAD_STUBS */ - -/* Define to 1 if you have the 'ptsname' function. */ -#define HAVE_PTSNAME 1 - -/* Define to 1 if you have the 'ptsname_r' function. */ -#define HAVE_PTSNAME_R 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_PTY_H 1 - -/* Define to 1 if you have the 'pwrite' function. */ -#define HAVE_PWRITE 1 - -/* Define to 1 if you have the 'pwritev' function. */ -#define HAVE_PWRITEV 1 - -/* Define to 1 if you have the 'pwritev2' function. */ -/* #undef HAVE_PWRITEV2 */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_READLINE_READLINE_H */ - -/* Define to 1 if you have the 'readlink' function. */ -#define HAVE_READLINK 1 - -/* Define to 1 if you have the 'readlinkat' function. */ -#define HAVE_READLINKAT 1 - -/* Define to 1 if you have the 'readv' function. */ -#define HAVE_READV 1 - -/* Define to 1 if you have the 'realpath' function. */ -#define HAVE_REALPATH 1 - -/* Define if you have the 'recvfrom' function. */ -#define HAVE_RECVFROM 1 - -/* Define to 1 if you have the 'renameat' function. */ -#define HAVE_RENAMEAT 1 - -/* Define if readline supports append_history */ -/* #undef HAVE_RL_APPEND_HISTORY */ - -/* Define if you can turn off readline's signal handling. */ -/* #undef HAVE_RL_CATCH_SIGNAL */ - -/* Define to 1 if the system has the type 'rl_compdisp_func_t'. */ -/* #undef HAVE_RL_COMPDISP_FUNC_T */ - -/* Define if you have readline 2.2 */ -/* #undef HAVE_RL_COMPLETION_APPEND_CHARACTER */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_COMPLETION_DISPLAY_MATCHES_HOOK */ - -/* Define if you have readline 4.2 */ -/* #undef HAVE_RL_COMPLETION_MATCHES */ - -/* Define if you have rl_completion_suppress_append */ -/* #undef HAVE_RL_COMPLETION_SUPPRESS_APPEND */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_PRE_INPUT_HOOK */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_RESIZE_TERMINAL */ - -/* Define to 1 if you have the 'rtpSpawn' function. */ -/* #undef HAVE_RTPSPAWN */ - -/* Define to 1 if you have the 'sched_get_priority_max' function. */ -#define HAVE_SCHED_GET_PRIORITY_MAX 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SCHED_H 1 - -/* Define to 1 if you have the 'sched_rr_get_interval' function. */ -#define HAVE_SCHED_RR_GET_INTERVAL 1 - -/* Define to 1 if you have the 'sched_setaffinity' function. */ -#define HAVE_SCHED_SETAFFINITY 1 - -/* Define to 1 if you have the 'sched_setparam' function. */ -#define HAVE_SCHED_SETPARAM 1 - -/* Define to 1 if you have the 'sched_setscheduler' function. */ -#define HAVE_SCHED_SETSCHEDULER 1 - -/* Define to 1 if you have the 'sem_clockwait' function. */ -/* #undef HAVE_SEM_CLOCKWAIT */ - -/* Define to 1 if you have the 'sem_getvalue' function. */ -#define HAVE_SEM_GETVALUE 1 - -/* Define to 1 if you have the 'sem_open' function. */ -/* #undef HAVE_SEM_OPEN */ - -/* Define to 1 if you have the 'sem_timedwait' function. */ -#define HAVE_SEM_TIMEDWAIT 1 - -/* Define to 1 if you have the 'sem_unlink' function. */ -/* #undef HAVE_SEM_UNLINK */ - -/* Define to 1 if you have the 'sendfile' function. */ -#define HAVE_SENDFILE 1 - -/* Define if you have the 'sendto' function. */ -#define HAVE_SENDTO 1 - -/* Define to 1 if you have the 'setegid' function. */ -/* #undef HAVE_SETEGID */ - -/* Define to 1 if you have the 'seteuid' function. */ -/* #undef HAVE_SETEUID */ - -/* Define to 1 if you have the 'setgid' function. */ -/* #undef HAVE_SETGID */ - -/* Define if you have the 'setgroups' function. */ -#define HAVE_SETGROUPS 1 - -/* Define to 1 if you have the 'sethostname' function. */ -/* #undef HAVE_SETHOSTNAME */ - -/* Define to 1 if you have the 'setitimer' function. */ -#define HAVE_SETITIMER 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SETJMP_H 1 - -/* Define to 1 if you have the 'setlocale' function. */ -#define HAVE_SETLOCALE 1 - -/* Define to 1 if you have the 'setns' function. */ -#define HAVE_SETNS 1 - -/* Define to 1 if you have the 'setpgid' function. */ -#define HAVE_SETPGID 1 - -/* Define to 1 if you have the 'setpgrp' function. */ -#define HAVE_SETPGRP 1 - -/* Define to 1 if you have the 'setpriority' function. */ -#define HAVE_SETPRIORITY 1 - -/* Define to 1 if you have the 'setregid' function. */ -/* #undef HAVE_SETREGID */ - -/* Define to 1 if you have the 'setresgid' function. */ -/* #undef HAVE_SETRESGID */ - -/* Define to 1 if you have the 'setresuid' function. */ -/* #undef HAVE_SETRESUID */ - -/* Define to 1 if you have the 'setreuid' function. */ -/* #undef HAVE_SETREUID */ - -/* Define to 1 if you have the 'setsid' function. */ -#define HAVE_SETSID 1 - -/* Define if you have the 'setsockopt' function. */ -#define HAVE_SETSOCKOPT 1 - -/* Define to 1 if you have the 'setuid' function. */ -/* #undef HAVE_SETUID */ - -/* Define to 1 if you have the 'setvbuf' function. */ -#define HAVE_SETVBUF 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SHADOW_H */ - -/* Define to 1 if you have the 'shm_open' function. */ -/* #undef HAVE_SHM_OPEN */ - -/* Define to 1 if you have the 'shm_unlink' function. */ -/* #undef HAVE_SHM_UNLINK */ - -/* Define to 1 if you have the 'shutdown' function. */ -#define HAVE_SHUTDOWN 1 - -/* Define to 1 if you have the 'sigaction' function. */ -#define HAVE_SIGACTION 1 - -/* Define to 1 if you have the 'sigaltstack' function. */ -#define HAVE_SIGALTSTACK 1 - -/* Define to 1 if you have the 'sigfillset' function. */ -#define HAVE_SIGFILLSET 1 - -/* Define to 1 if 'si_band' is a member of 'siginfo_t'. */ -#define HAVE_SIGINFO_T_SI_BAND 1 - -/* Define to 1 if you have the 'siginterrupt' function. */ -#define HAVE_SIGINTERRUPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SIGNAL_H 1 - -/* Define to 1 if you have the 'sigpending' function. */ -#define HAVE_SIGPENDING 1 - -/* Define to 1 if you have the 'sigrelse' function. */ -/* #undef HAVE_SIGRELSE */ - -/* Define to 1 if you have the 'sigtimedwait' function. */ -#define HAVE_SIGTIMEDWAIT 1 - -/* Define to 1 if you have the 'sigwait' function. */ -#define HAVE_SIGWAIT 1 - -/* Define to 1 if you have the 'sigwaitinfo' function. */ -#define HAVE_SIGWAITINFO 1 - -/* Define to 1 if you have the 'snprintf' function. */ -#define HAVE_SNPRINTF 1 - -/* struct sockaddr_alg (linux/if_alg.h) */ -#define HAVE_SOCKADDR_ALG 1 - -/* Define if sockaddr has sa_len member */ -/* #undef HAVE_SOCKADDR_SA_LEN */ - -/* struct sockaddr_storage (sys/socket.h) */ -#define HAVE_SOCKADDR_STORAGE 1 - -/* Define if you have the 'socket' function. */ -#define HAVE_SOCKET 1 - -/* Define if you have the 'socketpair' function. */ -#define HAVE_SOCKETPAIR 1 - -/* Define to 1 if the system has the type 'socklen_t'. */ -#define HAVE_SOCKLEN_T 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SPAWN_H 1 - -/* Define to 1 if you have the 'splice' function. */ -#define HAVE_SPLICE 1 - -/* Define to 1 if the system has the type 'ssize_t'. */ -#define HAVE_SSIZE_T 1 - -/* Define to 1 if you have the 'statvfs' function. */ -#define HAVE_STATVFS 1 - -/* Define if you have struct stat.st_mtim.tv_nsec */ -#define HAVE_STAT_TV_NSEC 1 - -/* Define if you have struct stat.st_mtimensec */ -/* #undef HAVE_STAT_TV_NSEC2 */ - -/* Define to 1 if you have the header file. */ -#define HAVE_STDINT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDLIB_H 1 - -/* Has stdatomic.h with atomic_int and atomic_uintptr_t */ -#define HAVE_STD_ATOMIC 1 - -/* Define to 1 if you have the 'strftime' function. */ -#define HAVE_STRFTIME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRINGS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRING_H 1 - -/* Define to 1 if you have the 'strlcpy' function. */ -#define HAVE_STRLCPY 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_STROPTS_H */ - -/* Define to 1 if you have the 'strsignal' function. */ -#define HAVE_STRSIGNAL 1 - -/* Define to 1 if 'pw_gecos' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_GECOS 1 - -/* Define to 1 if 'pw_passwd' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_PASSWD 1 - -/* Define to 1 if 'st_birthtime' is a member of 'struct stat'. */ -/* #undef HAVE_STRUCT_STAT_ST_BIRTHTIME */ - -/* Define to 1 if 'st_blksize' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLKSIZE 1 - -/* Define to 1 if 'st_blocks' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLOCKS 1 - -/* Define to 1 if 'st_flags' is a member of 'struct stat'. */ -/* #undef HAVE_STRUCT_STAT_ST_FLAGS */ - -/* Define to 1 if 'st_gen' is a member of 'struct stat'. */ -/* #undef HAVE_STRUCT_STAT_ST_GEN */ - -/* Define to 1 if 'st_rdev' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_RDEV 1 - -/* Define to 1 if 'tm_zone' is a member of 'struct tm'. */ -#define HAVE_STRUCT_TM_TM_ZONE 1 - -/* Define if you have the 'symlink' function. */ -#define HAVE_SYMLINK 1 - -/* Define to 1 if you have the 'symlinkat' function. */ -#define HAVE_SYMLINKAT 1 - -/* Define to 1 if you have the 'sync' function. */ -#define HAVE_SYNC 1 - -/* Define to 1 if you have the 'sysconf' function. */ -#define HAVE_SYSCONF 1 - -/* Define to 1 if you have the 'sysctlbyname' function. */ -/* #undef HAVE_SYSCTLBYNAME */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSEXITS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSLOG_H 1 - -/* Define to 1 if you have the 'system' function. */ -#define HAVE_SYSTEM 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_AUDIOIO_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_AUXV_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_BSDTTY_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_DEVPOLL_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_DIR_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_ENDIAN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_EPOLL_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_EVENTFD_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_EVENT_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_FILE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_IOCTL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_KERN_CONTROL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_LOADAVG_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_LOCK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MEMFD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MKDEV_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_MMAN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MODEM_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_NDIR_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_PARAM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_PIDFD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_POLL_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_RANDOM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_RESOURCE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SELECT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SENDFILE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SOCKET_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SOUNDCARD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STATVFS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STAT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SYSCALL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYSCTL_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SYSMACROS_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYS_DOMAIN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_TERMIO_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIMERFD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIMES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TYPES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UTSNAME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_WAIT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_XATTR_H 1 - -/* Define to 1 if you have the 'tcgetpgrp' function. */ -#define HAVE_TCGETPGRP 1 - -/* Define to 1 if you have the 'tcsetpgrp' function. */ -#define HAVE_TCSETPGRP 1 - -/* Define to 1 if you have the 'tempnam' function. */ -#define HAVE_TEMPNAM 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_TERMIOS_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_TERM_H */ - -/* Define to 1 if you have the 'timegm' function. */ -#define HAVE_TIMEGM 1 - -/* Define if you have the 'timerfd_create' function. */ -#define HAVE_TIMERFD_CREATE 1 - -/* Define to 1 if you have the 'times' function. */ -#define HAVE_TIMES 1 - -/* Define to 1 if you have the 'tmpfile' function. */ -#define HAVE_TMPFILE 1 - -/* Define to 1 if you have the 'tmpnam' function. */ -#define HAVE_TMPNAM 1 - -/* Define to 1 if you have the 'tmpnam_r' function. */ -/* #undef HAVE_TMPNAM_R */ - -/* Define to 1 if your 'struct tm' has 'tm_zone'. Deprecated, use - 'HAVE_STRUCT_TM_TM_ZONE' instead. */ -#define HAVE_TM_ZONE 1 - -/* Define to 1 if you have the 'truncate' function. */ -#define HAVE_TRUNCATE 1 - -/* Define to 1 if you have the 'ttyname_r' function. */ -#define HAVE_TTYNAME_R 1 - -/* Define to 1 if you don't have 'tm_zone' but do have the external array - 'tzname'. */ -/* #undef HAVE_TZNAME */ - -/* Define to 1 if you have the 'umask' function. */ -#define HAVE_UMASK 1 - -/* Define to 1 if you have the 'uname' function. */ -#define HAVE_UNAME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UNISTD_H 1 - -/* Define to 1 if you have the 'unlinkat' function. */ -#define HAVE_UNLINKAT 1 - -/* Define to 1 if you have the 'unlockpt' function. */ -#define HAVE_UNLOCKPT 1 - -/* Define to 1 if you have the 'unshare' function. */ -#define HAVE_UNSHARE 1 - -/* Define if you have a useable wchar_t type defined in wchar.h; useable means - wchar_t must be an unsigned type with at least 16 bits. (see - Include/unicodeobject.h). */ -/* #undef HAVE_USABLE_WCHAR_T */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UTIL_H */ - -/* Define to 1 if you have the 'utimensat' function. */ -#define HAVE_UTIMENSAT 1 - -/* Define to 1 if you have the 'utimes' function. */ -#define HAVE_UTIMES 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UTIME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UTMP_H 1 - -/* Define if you have the 'HAVE_UT_NAMESIZE' constant. */ -#define HAVE_UT_NAMESIZE 1 - -/* Define to 1 if you have the 'uuid_create' function. */ -/* #undef HAVE_UUID_CREATE */ - -/* Define to 1 if you have the 'uuid_enc_be' function. */ -/* #undef HAVE_UUID_ENC_BE */ - -/* Define if uuid_generate_time_safe() exists. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE */ - -/* Define if uuid_generate_time_safe() is able to deduce a MAC address. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE_STABLE_MAC */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UUID_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UUID_UUID_H */ - -/* Define to 1 if you have the 'vfork' function. */ -#define HAVE_VFORK 1 - -/* Define to 1 if you have the 'wait' function. */ -#define HAVE_WAIT 1 - -/* Define to 1 if you have the 'wait3' function. */ -/* #undef HAVE_WAIT3 */ - -/* Define to 1 if you have the 'wait4' function. */ -#define HAVE_WAIT4 1 - -/* Define to 1 if you have the 'waitid' function. */ -#define HAVE_WAITID 1 - -/* Define to 1 if you have the 'waitpid' function. */ -#define HAVE_WAITPID 1 - -/* Define if the compiler provides a wchar.h header file. */ -#define HAVE_WCHAR_H 1 - -/* Define to 1 if you have the 'wcscoll' function. */ -#define HAVE_WCSCOLL 1 - -/* Define to 1 if you have the 'wcsftime' function. */ -#define HAVE_WCSFTIME 1 - -/* Define to 1 if you have the 'wcsxfrm' function. */ -#define HAVE_WCSXFRM 1 - -/* Define to 1 if you have the 'wmemcmp' function. */ -#define HAVE_WMEMCMP 1 - -/* Define if tzset() actually switches the local timezone in a meaningful way. - */ -/* #undef HAVE_WORKING_TZSET */ - -/* Define to 1 if you have the 'writev' function. */ -#define HAVE_WRITEV 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZDICT_H */ - -/* Define if the zlib library has inflateCopy */ -#define HAVE_ZLIB_COPY 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ZLIB_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZSTD_H */ - -/* Define to 1 if you have the '_getpty' function. */ -/* #undef HAVE__GETPTY */ - -/* Define to 1 if the system has the type '__uint128_t'. */ -#define HAVE___UINT128_T 1 - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in . - */ -/* #undef MAJOR_IN_MKDEV */ - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in - . */ -#define MAJOR_IN_SYSMACROS 1 - -/* Define if mvwdelch in curses.h is an expression. */ -/* #undef MVWDELCH_IS_EXPRESSION */ - -/* Define to the address where bug reports for this package should be sent. */ -/* #undef PACKAGE_BUGREPORT */ - -/* Define to the full name of this package. */ -/* #undef PACKAGE_NAME */ - -/* Define to the full name and version of this package. */ -/* #undef PACKAGE_STRING */ - -/* Define to the one symbol short name of this package. */ -/* #undef PACKAGE_TARNAME */ - -/* Define to the home page for this package. */ -/* #undef PACKAGE_URL */ - -/* Define to the version of this package. */ -/* #undef PACKAGE_VERSION */ - -/* Define if POSIX semaphores aren't enabled on your system */ -/* #undef POSIX_SEMAPHORES_NOT_ENABLED */ - -/* Define if pthread_key_t is compatible with int. */ -#define PTHREAD_KEY_T_IS_COMPATIBLE_WITH_INT 1 - -/* Defined if PTHREAD_SCOPE_SYSTEM supported. */ -/* #undef PTHREAD_SYSTEM_SCHED_SUPPORTED */ - -/* Define as the preferred size in bits of long digits */ -/* #undef PYLONG_BITS_IN_DIGIT */ - -/* enabled builtin hash modules */ -#define PY_BUILTIN_HASHLIB_HASHES "md5,sha1,sha2,sha3,blake2" - -/* Define if you want to coerce the C locale to a UTF-8 based locale */ -#define PY_COERCE_C_LOCALE 1 - -/* Define to 1 if you have the perf trampoline. */ -/* #undef PY_HAVE_PERF_TRAMPOLINE */ - -/* Define to 1 to build the sqlite module with loadable extensions support. */ -/* #undef PY_SQLITE_ENABLE_LOAD_EXTENSION */ - -/* Define if SQLite was compiled with the serialize API */ -#define PY_SQLITE_HAVE_SERIALIZE 1 - -/* Default cipher suites list for ssl module. 1: Python's preferred selection, - 2: leave OpenSSL defaults untouched, 0: custom string */ -#define PY_SSL_DEFAULT_CIPHERS 1 - -/* Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0 */ -/* #undef PY_SSL_DEFAULT_CIPHER_STRING */ - -/* PEP 11 Support tier (1, 2, 3 or 0 for unsupported) */ -#define PY_SUPPORT_TIER 3 - -/* Define if you want to build an interpreter with many run-time checks. */ -/* #undef Py_DEBUG */ - -/* Defined if Python is built as a shared library. */ -#define Py_ENABLE_SHARED 1 - -/* Define if you want to disable the GIL */ -/* #undef Py_GIL_DISABLED */ - -/* Define hash algorithm for str, bytes and memoryview. SipHash24: 1, FNV: 2, - SipHash13: 3, externally defined: 0 */ -/* #undef Py_HASH_ALGORITHM */ - -/* Define if you want to enable remote debugging support. */ -#define Py_REMOTE_DEBUG 1 - -/* Define if rl_startup_hook takes arguments */ -/* #undef Py_RL_STARTUP_HOOK_TAKES_ARGS */ - -/* Define if you want to enable internal statistics gathering. */ -/* #undef Py_STATS */ - -/* The version of SunOS/Solaris as reported by `uname -r' without the dot. */ -/* #undef Py_SUNOS_VERSION */ - -/* Define if you want to use tail-calling interpreters in CPython. */ -/* #undef Py_TAIL_CALL_INTERP */ - -/* Define if you want to enable tracing references for debugging purpose */ -/* #undef Py_TRACE_REFS */ - -/* assume C89 semantics that RETSIGTYPE is always void */ -#define RETSIGTYPE void - -/* Define if setpgrp() must be called as setpgrp(0, 0). */ -/* #undef SETPGRP_HAVE_ARG */ - -/* Define if i>>j for signed int i does not extend the sign bit when i < 0 */ -/* #undef SIGNED_RIGHT_SHIFT_ZERO_FILLS */ - -/* The size of 'double', as computed by sizeof. */ -#define SIZEOF_DOUBLE 8 - -/* The size of 'float', as computed by sizeof. */ -#define SIZEOF_FLOAT 4 - -/* The size of 'fpos_t', as computed by sizeof. */ -#define SIZEOF_FPOS_T 8 - -/* The size of 'int', as computed by sizeof. */ -#define SIZEOF_INT 4 - -/* The size of 'long', as computed by sizeof. */ -#define SIZEOF_LONG 8 - -/* The size of 'long double', as computed by sizeof. */ -#define SIZEOF_LONG_DOUBLE 16 - -/* The size of 'long long', as computed by sizeof. */ -#define SIZEOF_LONG_LONG 8 - -/* The size of 'off_t', as computed by sizeof. */ -#define SIZEOF_OFF_T 8 - -/* The size of 'pid_t', as computed by sizeof. */ -#define SIZEOF_PID_T 4 - -/* The size of 'pthread_key_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_KEY_T 4 - -/* The size of 'pthread_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_T 8 - -/* The size of 'short', as computed by sizeof. */ -#define SIZEOF_SHORT 2 - -/* The size of 'size_t', as computed by sizeof. */ -#define SIZEOF_SIZE_T 8 - -/* The size of 'time_t', as computed by sizeof. */ -#define SIZEOF_TIME_T 8 - -/* The size of 'uintptr_t', as computed by sizeof. */ -#define SIZEOF_UINTPTR_T 8 - -/* The size of 'void *', as computed by sizeof. */ -#define SIZEOF_VOID_P 8 - -/* The size of 'wchar_t', as computed by sizeof. */ -#define SIZEOF_WCHAR_T 4 - -/* The size of '_Bool', as computed by sizeof. */ -#define SIZEOF__BOOL 1 - -/* Define to 1 if you have the ANSI C header files. */ -#define STDC_HEADERS 1 - -/* Define if you can safely include both and - (which you can't on SCO ODT 3.0). */ -#define SYS_SELECT_WITH_SYS_TIME 1 - -/* Custom thread stack size depending on chosen sanitizer runtimes. */ -/* #undef THREAD_STACK_SIZE */ - -/* Library needed by timemodule.c: librt may be needed for clock_gettime() */ -/* #undef TIMEMODULE_LIB */ - -/* Define to 1 if your declares 'struct tm'. */ -/* #undef TM_IN_SYS_TIME */ - -/* Define if you want to use computed gotos in ceval.c. */ -/* #undef USE_COMPUTED_GOTOS */ - -/* Enable extensions on AIX, Interix, z/OS. */ -#ifndef _ALL_SOURCE -# define _ALL_SOURCE 1 -#endif -/* Enable general extensions on macOS. */ -#ifndef _DARWIN_C_SOURCE -# define _DARWIN_C_SOURCE 1 -#endif -/* Enable general extensions on Solaris. */ -#ifndef __EXTENSIONS__ -# define __EXTENSIONS__ 1 -#endif -/* Enable GNU extensions on systems that have them. */ -#ifndef _GNU_SOURCE -# define _GNU_SOURCE 1 -#endif -/* Enable X/Open compliant socket functions that do not require linking - with -lxnet on HP-UX 11.11. */ -#ifndef _HPUX_ALT_XOPEN_SOCKET_API -# define _HPUX_ALT_XOPEN_SOCKET_API 1 -#endif -/* Identify the host operating system as Minix. - This macro does not affect the system headers' behavior. - A future release of Autoconf may stop defining this macro. */ -#ifndef _MINIX -/* # undef _MINIX */ -#endif -/* Enable general extensions on NetBSD. - Enable NetBSD compatibility extensions on Minix. */ -#ifndef _NETBSD_SOURCE -# define _NETBSD_SOURCE 1 -#endif -/* Enable OpenBSD compatibility extensions on NetBSD. - Oddly enough, this does nothing on OpenBSD. */ -#ifndef _OPENBSD_SOURCE -# define _OPENBSD_SOURCE 1 -#endif -/* Define to 1 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_SOURCE -/* # undef _POSIX_SOURCE */ -#endif -/* Define to 2 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_1_SOURCE -/* # undef _POSIX_1_SOURCE */ -#endif -/* Enable POSIX-compatible threading on Solaris. */ -#ifndef _POSIX_PTHREAD_SEMANTICS -# define _POSIX_PTHREAD_SEMANTICS 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-5:2014. */ -#ifndef __STDC_WANT_IEC_60559_ATTRIBS_EXT__ -# define __STDC_WANT_IEC_60559_ATTRIBS_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-1:2014. */ -#ifndef __STDC_WANT_IEC_60559_BFP_EXT__ -# define __STDC_WANT_IEC_60559_BFP_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-2:2015. */ -#ifndef __STDC_WANT_IEC_60559_DFP_EXT__ -# define __STDC_WANT_IEC_60559_DFP_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex F. */ -#ifndef __STDC_WANT_IEC_60559_EXT__ -# define __STDC_WANT_IEC_60559_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-4:2015. */ -#ifndef __STDC_WANT_IEC_60559_FUNCS_EXT__ -# define __STDC_WANT_IEC_60559_FUNCS_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex H and ISO/IEC TS 18661-3:2015. */ -#ifndef __STDC_WANT_IEC_60559_TYPES_EXT__ -# define __STDC_WANT_IEC_60559_TYPES_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TR 24731-2:2010. */ -#ifndef __STDC_WANT_LIB_EXT2__ -# define __STDC_WANT_LIB_EXT2__ 1 -#endif -/* Enable extensions specified by ISO/IEC 24747:2009. */ -#ifndef __STDC_WANT_MATH_SPEC_FUNCS__ -# define __STDC_WANT_MATH_SPEC_FUNCS__ 1 -#endif -/* Enable extensions on HP NonStop. */ -#ifndef _TANDEM_SOURCE -# define _TANDEM_SOURCE 1 -#endif -/* Enable X/Open extensions. Define to 500 only if necessary - to make mbstate_t available. */ -#ifndef _XOPEN_SOURCE -# define _XOPEN_SOURCE 700 -#endif - - -/* Define if WINDOW in curses.h offers a field _flags. */ -/* #undef WINDOW_HAS_FLAGS */ - -/* Define if you want build the _decimal module using a coroutine-local rather - than a thread-local context */ -#define WITH_DECIMAL_CONTEXTVAR 1 - -/* Define if you want documentation strings in extension modules */ -#define WITH_DOC_STRINGS 1 - -/* Define if you want to compile in DTrace support */ -/* #undef WITH_DTRACE */ - -/* Define if you want to use the new-style (Openstep, Rhapsody, MacOS) dynamic - linker (dyld) instead of the old-style (NextStep) dynamic linker (rld). - Dyld is necessary to support frameworks. */ -/* #undef WITH_DYLD */ - -/* Define to build the readline module against libedit. */ -/* #undef WITH_EDITLINE */ - -/* Define to 1 if libintl is needed for locale functions. */ -/* #undef WITH_LIBINTL */ - -/* Define if you want to compile in mimalloc memory allocator. */ -#define WITH_MIMALLOC 1 - -/* Define if you want to produce an OpenStep/Rhapsody framework (shared - library plus accessory files). */ -/* #undef WITH_NEXT_FRAMEWORK */ - -/* Define if you want to compile in Python-specific mallocs */ -#define WITH_PYMALLOC 1 - -/* Define if you want pymalloc to be disabled when running under valgrind */ -/* #undef WITH_VALGRIND */ - -/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most - significant byte first (like Motorola and SPARC, unlike Intel). */ -#if defined AC_APPLE_UNIVERSAL_BUILD -# if defined __BIG_ENDIAN__ -# define WORDS_BIGENDIAN 1 -# endif -#else -# ifndef WORDS_BIGENDIAN -/* # undef WORDS_BIGENDIAN */ -# endif -#endif - -/* Define if arithmetic is subject to x87-style double rounding issue */ -/* #undef X87_DOUBLE_ROUNDING */ - -/* Define on OpenBSD to activate all library features */ -/* #undef _BSD_SOURCE */ - -/* Define on Darwin to activate all library features */ -#define _DARWIN_C_SOURCE 1 - -/* This must be set to 64 on some systems to enable large file support. */ -#define _FILE_OFFSET_BITS 64 - -/* Define to include mbstate_t for mbrtowc */ -/* #undef _INCLUDE__STDC_A1_SOURCE */ - -/* This must be defined on some systems to enable large file support. */ -#define _LARGEFILE_SOURCE 1 - -/* This must be defined on AIX systems to enable large file support. */ -/* #undef _LARGE_FILES */ - -/* Define on NetBSD to activate all library features */ -#define _NETBSD_SOURCE 1 - -/* Define to activate features from IEEE Stds 1003.1-2008 */ -#define _POSIX_C_SOURCE 200809L - -/* Define if you have POSIX threads, and your system does not define that. */ -/* #undef _POSIX_THREADS */ - -/* framework name */ -#define _PYTHONFRAMEWORK "" - -/* Maximum length in bytes of a thread name */ -#define _PYTHREAD_NAME_MAXLEN 15 - -/* Defined if _Complex C type can be used with libffi. */ -/* #undef _Py_FFI_SUPPORT_C_COMPLEX */ - -/* HACL* library can compile SIMD128 implementations */ -/* #undef _Py_HACL_CAN_COMPILE_VEC128 */ - -/* HACL* library can compile SIMD256 implementations */ -/* #undef _Py_HACL_CAN_COMPILE_VEC256 */ - -/* Thread stack size set by the linker (in bytes). */ -/* #undef _Py_LINKER_THREAD_STACK_SIZE */ - -/* Define to 1 if the machine stack grows down (default); 0 if it grows up. */ -#define _Py_STACK_GROWS_DOWN 1 - -/* Define to force use of thread-safe errno, h_errno, and other functions */ -#define _REENTRANT 1 - -/* Define to 1 if you want to emulate getpid() on WASI */ -/* #undef _WASI_EMULATED_GETPID */ - -/* Define to 1 if you want to emulate process clocks on WASI */ -/* #undef _WASI_EMULATED_PROCESS_CLOCKS */ - -/* Define to 1 if you want to emulate signals on WASI */ -/* #undef _WASI_EMULATED_SIGNAL */ - -/* Define to the level of X/Open that your system supports */ -#define _XOPEN_SOURCE 700 - -/* Define to activate Unix95-and-earlier features */ -#define _XOPEN_SOURCE_EXTENDED 1 - -/* Define on FreeBSD to activate all library features */ -#define __BSD_VISIBLE 1 - -/* Define to 'long' if does not define clock_t. */ -/* #undef clock_t */ - -/* Define to empty if 'const' does not conform to ANSI C. */ -/* #undef const */ - -/* Define as 'int' if doesn't define. */ -/* #undef gid_t */ - -/* Define to 'int' if does not define. */ -/* #undef mode_t */ - -/* Define to 'long int' if does not define. */ -/* #undef off_t */ - -/* Define as a signed integer type capable of holding a process identifier. */ -/* #undef pid_t */ - -/* Define to empty if the keyword does not work. */ -/* #undef signed */ - -/* Define as 'unsigned int' if doesn't define. */ -/* #undef size_t */ - -/* Define to 'int' if does not define. */ -/* #undef socklen_t */ - -/* Define as 'int' if doesn't define. */ -/* #undef uid_t */ - - -/* Define the macros needed if on a UnixWare 7.x system. */ -#if defined(__USLC__) && defined(__SCO_VERSION__) -#define STRICT_SYSV_CURSES /* Don't use ncurses extensions */ -#endif - -#endif /*Py_PYCONFIG_H*/ - diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-android-x86_64.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-android-x86_64.h deleted file mode 100644 index 45d569daa..000000000 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-android-x86_64.h +++ /dev/null @@ -1,2115 +0,0 @@ -/* pyconfig.h. Generated from pyconfig.h.in by configure. */ -/* pyconfig.h.in. Generated from configure.ac by autoheader. */ - - -#ifndef Py_PYCONFIG_H -#define Py_PYCONFIG_H - - -/* Define if building universal (internal helper macro) */ -/* #undef AC_APPLE_UNIVERSAL_BUILD */ - -/* BUILD_GNU_TYPE + AIX_BUILDDATE are used to construct the PEP425 tag of the - build system. */ -/* #undef AIX_BUILDDATE */ - -/* Define for AIX if your compiler is a genuine IBM xlC/xlC_r and you want - support for AIX C++ shared extension modules. */ -/* #undef AIX_GENUINE_CPLUSPLUS */ - -/* The normal alignment of 'long', in bytes. */ -#define ALIGNOF_LONG 8 - -/* The normal alignment of 'max_align_t', in bytes. */ -#define ALIGNOF_MAX_ALIGN_T 16 - -/* The normal alignment of 'size_t', in bytes. */ -#define ALIGNOF_SIZE_T 8 - -/* Alternative SOABI used in debug build to load C extensions built in release - mode */ -/* #undef ALT_SOABI */ - -/* The Android API level. */ -#define ANDROID_API_LEVEL 24 - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored in ARM - mixed-endian order (byte order 45670123) */ -/* #undef DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the most - significant byte first */ -/* #undef DOUBLE_IS_BIG_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the - least significant byte first */ -#define DOUBLE_IS_LITTLE_ENDIAN_IEEE754 1 - -/* Define if --enable-ipv6 is specified */ -#define ENABLE_IPV6 1 - -/* Define if getpgrp() must be called as getpgrp(0). */ -/* #undef GETPGRP_HAVE_ARG */ - -/* Define if you have the 'accept' function. */ -#define HAVE_ACCEPT 1 - -/* Define to 1 if you have the 'accept4' function. */ -#define HAVE_ACCEPT4 1 - -/* Define to 1 if you have the 'acosh' function. */ -#define HAVE_ACOSH 1 - -/* struct addrinfo (netdb.h) */ -#define HAVE_ADDRINFO 1 - -/* Define to 1 if you have the 'alarm' function. */ -#define HAVE_ALARM 1 - -/* Define if aligned memory access is required */ -/* #undef HAVE_ALIGNED_REQUIRED */ - -/* Define to 1 if you have the header file. */ -#define HAVE_ALLOCA_H 1 - -/* Define this if your time.h defines altzone. */ -/* #undef HAVE_ALTZONE */ - -/* Define to 1 if you have the 'asinh' function. */ -#define HAVE_ASINH 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ASM_TYPES_H 1 - -/* Define to 1 if you have the 'atanh' function. */ -#define HAVE_ATANH 1 - -/* Define to 1 if you have the 'backtrace' function. */ -/* #undef HAVE_BACKTRACE */ - -/* Define if you have the 'bind' function. */ -#define HAVE_BIND 1 - -/* Define to 1 if you have the 'bind_textdomain_codeset' function. */ -/* #undef HAVE_BIND_TEXTDOMAIN_CODESET */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_BLUETOOTH_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_H */ - -/* Define if mbstowcs(NULL, "text", 0) does not return the number of wide - chars that would be converted. */ -/* #undef HAVE_BROKEN_MBSTOWCS */ - -/* Define if nice() returns success/failure instead of the new priority. */ -/* #undef HAVE_BROKEN_NICE */ - -/* Define if the system reports an invalid PIPE_BUF value. */ -/* #undef HAVE_BROKEN_PIPE_BUF */ - -/* Define if poll() sets errno on invalid file descriptors. */ -/* #undef HAVE_BROKEN_POLL */ - -/* Define if the Posix semaphores do not work on your system */ -/* #undef HAVE_BROKEN_POSIX_SEMAPHORES */ - -/* Define if pthread_sigmask() does not work on your system. */ -/* #undef HAVE_BROKEN_PTHREAD_SIGMASK */ - -/* define to 1 if your sem_getvalue is broken. */ -#define HAVE_BROKEN_SEM_GETVALUE 1 - -/* Define if 'unsetenv' does not return an int. */ -/* #undef HAVE_BROKEN_UNSETENV */ - -/* Has builtin __atomic_load_n() and __atomic_store_n() functions */ -#define HAVE_BUILTIN_ATOMIC 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_BZLIB_H 1 - -/* Define to 1 if you have the 'chflags' function. */ -/* #undef HAVE_CHFLAGS */ - -/* Define to 1 if you have the 'chmod' function. */ -#define HAVE_CHMOD 1 - -/* Define to 1 if you have the 'chown' function. */ -#define HAVE_CHOWN 1 - -/* Define if you have the 'chroot' function. */ -/* #undef HAVE_CHROOT */ - -/* Define to 1 if you have the 'clock' function. */ -#define HAVE_CLOCK 1 - -/* Define to 1 if you have the 'clock_getres' function. */ -#define HAVE_CLOCK_GETRES 1 - -/* Define to 1 if you have the 'clock_gettime' function. */ -#define HAVE_CLOCK_GETTIME 1 - -/* Define to 1 if you have the 'clock_nanosleep' function. */ -#define HAVE_CLOCK_NANOSLEEP 1 - -/* Define to 1 if you have the 'clock_settime' function. */ -/* #undef HAVE_CLOCK_SETTIME */ - -/* Define to 1 if the system has the type 'clock_t'. */ -#define HAVE_CLOCK_T 1 - -/* Define to 1 if you have the 'closefrom' function. */ -/* #undef HAVE_CLOSEFROM */ - -/* Define to 1 if you have the 'close_range' function. */ -/* #undef HAVE_CLOSE_RANGE */ - -/* Define if the C compiler supports computed gotos. */ -/* #undef HAVE_COMPUTED_GOTOS */ - -/* Define to 1 if you have the 'confstr' function. */ -/* #undef HAVE_CONFSTR */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CONIO_H */ - -/* Define if you have the 'connect' function. */ -#define HAVE_CONNECT 1 - -/* Define to 1 if you have the 'copy_file_range' function. */ -/* #undef HAVE_COPY_FILE_RANGE */ - -/* Define to 1 if you have the 'ctermid' function. */ -/* #undef HAVE_CTERMID */ - -/* Define if you have the 'ctermid_r' function. */ -/* #undef HAVE_CTERMID_R */ - -/* Define if you have the 'ESCDELAY' variable. */ -/* #undef HAVE_CURSES_ESCDELAY */ - -/* Define if you have the 'filter' function. */ -/* #undef HAVE_CURSES_FILTER */ - -/* Define if you have the 'getmouse' function with the X/Open signature. */ -/* #undef HAVE_CURSES_GETMOUSE */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CURSES_H */ - -/* Define if you have the 'has_key' function. */ -/* #undef HAVE_CURSES_HAS_KEY */ - -/* Define if you have the 'immedok' function. */ -/* #undef HAVE_CURSES_IMMEDOK */ - -/* Define if you have the 'is_pad' function. */ -/* #undef HAVE_CURSES_IS_PAD */ - -/* Define if you have the 'is_term_resized' function. */ -/* #undef HAVE_CURSES_IS_TERM_RESIZED */ - -/* Define if you have the 'resizeterm' function. */ -/* #undef HAVE_CURSES_RESIZETERM */ - -/* Define if you have the 'resize_term' function. */ -/* #undef HAVE_CURSES_RESIZE_TERM */ - -/* Define if you have the 'set_escdelay' function. */ -/* #undef HAVE_CURSES_SET_ESCDELAY */ - -/* Define if you have the 'set_tabsize' function. */ -/* #undef HAVE_CURSES_SET_TABSIZE */ - -/* Define if you have the 'syncok' function. */ -/* #undef HAVE_CURSES_SYNCOK */ - -/* Define if you have the 'TABSIZE' variable. */ -/* #undef HAVE_CURSES_TABSIZE */ - -/* Define if you have the 'typeahead' function. */ -/* #undef HAVE_CURSES_TYPEAHEAD */ - -/* Define if you have the 'use_env' function. */ -/* #undef HAVE_CURSES_USE_ENV */ - -/* Define if you have the 'wchgat' function. */ -/* #undef HAVE_CURSES_WCHGAT */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_DB_H */ - -/* Define to 1 if you have the declaration of 'RTLD_DEEPBIND', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_DEEPBIND 0 - -/* Define to 1 if you have the declaration of 'RTLD_GLOBAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_GLOBAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_LAZY', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LAZY 1 - -/* Define to 1 if you have the declaration of 'RTLD_LOCAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LOCAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_MEMBER', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_MEMBER 0 - -/* Define to 1 if you have the declaration of 'RTLD_NODELETE', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NODELETE 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOLOAD', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOLOAD 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOW', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOW 1 - -/* Define to 1 if you have the declaration of 'tzname', and to 0 if you don't. - */ -/* #undef HAVE_DECL_TZNAME */ - -/* Define to 1 if you have the declaration of 'UT_NAMESIZE', and to 0 if you - don't. */ -#define HAVE_DECL_UT_NAMESIZE 1 - -/* Define to 1 if you have the device macros. */ -#define HAVE_DEVICE_MACROS 1 - -/* Define to 1 if you have the /dev/ptc device file. */ -/* #undef HAVE_DEV_PTC */ - -/* Define to 1 if you have the /dev/ptmx device file. */ -/* #undef HAVE_DEV_PTMX */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_DIRECT_H */ - -/* Define to 1 if the dirent structure has a d_type field */ -#define HAVE_DIRENT_D_TYPE 1 - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -#define HAVE_DIRENT_H 1 - -/* Define if you have the 'dirfd' function or macro. */ -#define HAVE_DIRFD 1 - -/* Define to 1 if you have the 'dladdr' function. */ -#define HAVE_DLADDR 1 - -/* Define to 1 if you have the 'dladdr1' function. */ -/* #undef HAVE_DLADDR1 */ - -/* Define to 1 if you have the header file. */ -#define HAVE_DLFCN_H 1 - -/* Define to 1 if you have the 'dlopen' function. */ -#define HAVE_DLOPEN 1 - -/* Define to 1 if you have the 'dl_iterate_phdr' function. */ -#define HAVE_DL_ITERATE_PHDR 1 - -/* Define to 1 if you have the 'dup' function. */ -#define HAVE_DUP 1 - -/* Define to 1 if you have the 'dup2' function. */ -#define HAVE_DUP2 1 - -/* Define to 1 if you have the 'dup3' function. */ -#define HAVE_DUP3 1 - -/* Define if you have the '_dyld_shared_cache_contains_path' function. */ -/* #undef HAVE_DYLD_SHARED_CACHE_CONTAINS_PATH */ - -/* Defined when any dynamic module loading is enabled. */ -#define HAVE_DYNAMIC_LOADING 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_EDITLINE_READLINE_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_ENDIAN_H 1 - -/* Define if you have the 'epoll_create' function. */ -#define HAVE_EPOLL 1 - -/* Define if you have the 'epoll_create1' function. */ -#define HAVE_EPOLL_CREATE1 1 - -/* Define to 1 if you have the 'erf' function. */ -#define HAVE_ERF 1 - -/* Define to 1 if you have the 'erfc' function. */ -#define HAVE_ERFC 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ERRNO_H 1 - -/* Define if you have the 'eventfd' function. */ -#define HAVE_EVENTFD 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_EXECINFO_H 1 - -/* Define to 1 if you have the 'execv' function. */ -#define HAVE_EXECV 1 - -/* Define to 1 if you have the 'explicit_bzero' function. */ -/* #undef HAVE_EXPLICIT_BZERO */ - -/* Define to 1 if you have the 'explicit_memset' function. */ -/* #undef HAVE_EXPLICIT_MEMSET */ - -/* Define to 1 if you have the 'expm1' function. */ -#define HAVE_EXPM1 1 - -/* Define to 1 if you have the 'faccessat' function. */ -#define HAVE_FACCESSAT 1 - -/* Define if you have the 'fchdir' function. */ -#define HAVE_FCHDIR 1 - -/* Define to 1 if you have the 'fchmod' function. */ -#define HAVE_FCHMOD 1 - -/* Define to 1 if you have the 'fchmodat' function. */ -#define HAVE_FCHMODAT 1 - -/* Define to 1 if you have the 'fchown' function. */ -#define HAVE_FCHOWN 1 - -/* Define to 1 if you have the 'fchownat' function. */ -#define HAVE_FCHOWNAT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_FCNTL_H 1 - -/* Define if you have the 'fdatasync' function. */ -#define HAVE_FDATASYNC 1 - -/* Define to 1 if you have the 'fdopendir' function. */ -#define HAVE_FDOPENDIR 1 - -/* Define to 1 if you have the 'fdwalk' function. */ -/* #undef HAVE_FDWALK */ - -/* Define to 1 if you have the 'fexecve' function. */ -/* #undef HAVE_FEXECVE */ - -/* Define if you have the 'ffi_closure_alloc' function. */ -#define HAVE_FFI_CLOSURE_ALLOC 1 - -/* Define if you have the 'ffi_prep_cif_var' function. */ -#define HAVE_FFI_PREP_CIF_VAR 1 - -/* Define if you have the 'ffi_prep_closure_loc' function. */ -#define HAVE_FFI_PREP_CLOSURE_LOC 1 - -/* Define to 1 if you have the 'flock' function. */ -#define HAVE_FLOCK 1 - -/* Define to 1 if you have the 'fork' function. */ -#define HAVE_FORK 1 - -/* Define to 1 if you have the 'fork1' function. */ -/* #undef HAVE_FORK1 */ - -/* Define to 1 if you have the 'forkpty' function. */ -#define HAVE_FORKPTY 1 - -/* Define to 1 if you have the 'fpathconf' function. */ -#define HAVE_FPATHCONF 1 - -/* Define to 1 if you have the 'fseek64' function. */ -/* #undef HAVE_FSEEK64 */ - -/* Define to 1 if you have the 'fseeko' function. */ -#define HAVE_FSEEKO 1 - -/* Define to 1 if you have the 'fstatat' function. */ -#define HAVE_FSTATAT 1 - -/* Define to 1 if you have the 'fstatvfs' function. */ -#define HAVE_FSTATVFS 1 - -/* Define if you have the 'fsync' function. */ -#define HAVE_FSYNC 1 - -/* Define to 1 if you have the 'ftell64' function. */ -/* #undef HAVE_FTELL64 */ - -/* Define to 1 if you have the 'ftello' function. */ -#define HAVE_FTELLO 1 - -/* Define to 1 if you have the 'ftime' function. */ -/* #undef HAVE_FTIME */ - -/* Define to 1 if you have the 'ftruncate' function. */ -#define HAVE_FTRUNCATE 1 - -/* Define to 1 if you have the 'futimens' function. */ -#define HAVE_FUTIMENS 1 - -/* Define to 1 if you have the 'futimes' function. */ -/* #undef HAVE_FUTIMES */ - -/* Define to 1 if you have the 'futimesat' function. */ -/* #undef HAVE_FUTIMESAT */ - -/* Define to 1 if you have the 'gai_strerror' function. */ -#define HAVE_GAI_STRERROR 1 - -/* Define if we can use gcc inline assembler to get and set mc68881 fpcr */ -/* #undef HAVE_GCC_ASM_FOR_MC68881 */ - -/* Define if we can use x64 gcc inline assembler */ -#define HAVE_GCC_ASM_FOR_X64 1 - -/* Define if we can use gcc inline assembler to get and set x87 control word - */ -#define HAVE_GCC_ASM_FOR_X87 1 - -/* Define if your compiler provides __uint128_t */ -#define HAVE_GCC_UINT128_T 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_DASH_NDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_NDBM_H */ - -/* Define if you have the getaddrinfo function. */ -#define HAVE_GETADDRINFO 1 - -/* Define this if you have flockfile(), getc_unlocked(), and funlockfile() */ -#define HAVE_GETC_UNLOCKED 1 - -/* Define to 1 if you have the 'getegid' function. */ -#define HAVE_GETEGID 1 - -/* Define to 1 if you have the 'getentropy' function. */ -/* #undef HAVE_GETENTROPY */ - -/* Define to 1 if you have the 'geteuid' function. */ -#define HAVE_GETEUID 1 - -/* Define to 1 if you have the 'getgid' function. */ -#define HAVE_GETGID 1 - -/* Define to 1 if you have the 'getgrent' function. */ -/* #undef HAVE_GETGRENT */ - -/* Define to 1 if you have the 'getgrgid' function. */ -#define HAVE_GETGRGID 1 - -/* Define to 1 if you have the 'getgrgid_r' function. */ -#define HAVE_GETGRGID_R 1 - -/* Define to 1 if you have the 'getgrnam_r' function. */ -#define HAVE_GETGRNAM_R 1 - -/* Define to 1 if you have the 'getgrouplist' function. */ -#define HAVE_GETGROUPLIST 1 - -/* Define to 1 if you have the 'getgroups' function. */ -#define HAVE_GETGROUPS 1 - -/* Define if you have the 'gethostbyaddr' function. */ -#define HAVE_GETHOSTBYADDR 1 - -/* Define to 1 if you have the 'gethostbyname' function. */ -#define HAVE_GETHOSTBYNAME 1 - -/* Define this if you have some version of gethostbyname_r() */ -#define HAVE_GETHOSTBYNAME_R 1 - -/* Define this if you have the 3-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_3_ARG */ - -/* Define this if you have the 5-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_5_ARG */ - -/* Define this if you have the 6-arg version of gethostbyname_r(). */ -#define HAVE_GETHOSTBYNAME_R_6_ARG 1 - -/* Define to 1 if you have the 'gethostname' function. */ -#define HAVE_GETHOSTNAME 1 - -/* Define to 1 if you have the 'getitimer' function. */ -#define HAVE_GETITIMER 1 - -/* Define to 1 if you have the 'getloadavg' function. */ -/* #undef HAVE_GETLOADAVG */ - -/* Define to 1 if you have the 'getlogin' function. */ -#define HAVE_GETLOGIN 1 - -/* Define to 1 if you have the 'getlogin_r' function. */ -/* #undef HAVE_GETLOGIN_R */ - -/* Define to 1 if you have the 'getnameinfo' function. */ -#define HAVE_GETNAMEINFO 1 - -/* Define if you have the 'getpagesize' function. */ -#define HAVE_GETPAGESIZE 1 - -/* Define if you have the 'getpeername' function. */ -#define HAVE_GETPEERNAME 1 - -/* Define to 1 if you have the 'getpgid' function. */ -#define HAVE_GETPGID 1 - -/* Define to 1 if you have the 'getpgrp' function. */ -#define HAVE_GETPGRP 1 - -/* Define to 1 if you have the 'getpid' function. */ -#define HAVE_GETPID 1 - -/* Define to 1 if you have the 'getppid' function. */ -#define HAVE_GETPPID 1 - -/* Define to 1 if you have the 'getpriority' function. */ -#define HAVE_GETPRIORITY 1 - -/* Define if you have the 'getprotobyname' function. */ -#define HAVE_GETPROTOBYNAME 1 - -/* Define to 1 if you have the 'getpwent' function. */ -/* #undef HAVE_GETPWENT */ - -/* Define to 1 if you have the 'getpwnam_r' function. */ -#define HAVE_GETPWNAM_R 1 - -/* Define to 1 if you have the 'getpwuid' function. */ -#define HAVE_GETPWUID 1 - -/* Define to 1 if you have the 'getpwuid_r' function. */ -#define HAVE_GETPWUID_R 1 - -/* Define to 1 if the getrandom() function is available */ -/* #undef HAVE_GETRANDOM */ - -/* Define to 1 if the Linux getrandom() syscall is available */ -#define HAVE_GETRANDOM_SYSCALL 1 - -/* Define to 1 if you have the 'getresgid' function. */ -#define HAVE_GETRESGID 1 - -/* Define to 1 if you have the 'getresuid' function. */ -#define HAVE_GETRESUID 1 - -/* Define to 1 if you have the 'getrusage' function. */ -#define HAVE_GETRUSAGE 1 - -/* Define if you have the 'getservbyname' function. */ -#define HAVE_GETSERVBYNAME 1 - -/* Define if you have the 'getservbyport' function. */ -#define HAVE_GETSERVBYPORT 1 - -/* Define to 1 if you have the 'getsid' function. */ -#define HAVE_GETSID 1 - -/* Define if you have the 'getsockname' function. */ -#define HAVE_GETSOCKNAME 1 - -/* Define to 1 if you have the 'getspent' function. */ -/* #undef HAVE_GETSPENT */ - -/* Define to 1 if you have the 'getspnam' function. */ -/* #undef HAVE_GETSPNAM */ - -/* Define to 1 if you have the 'getuid' function. */ -#define HAVE_GETUID 1 - -/* Define to 1 if you have the 'getwd' function. */ -/* #undef HAVE_GETWD */ - -/* Define if glibc has incorrect _FORTIFY_SOURCE wrappers for memmove and - bcopy. */ -/* #undef HAVE_GLIBC_MEMMOVE_BUG */ - -/* Define to 1 if you have the 'grantpt' function. */ -#define HAVE_GRANTPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_GRP_H 1 - -/* Define if you have the 'hstrerror' function. */ -#define HAVE_HSTRERROR 1 - -/* Define this if you have le64toh() */ -#define HAVE_HTOLE64 1 - -/* Define to 1 if you have the 'if_nameindex' function. */ -#define HAVE_IF_NAMEINDEX 1 - -/* Define if you have the 'inet_aton' function. */ -#define HAVE_INET_ATON 1 - -/* Define if you have the 'inet_ntoa' function. */ -#define HAVE_INET_NTOA 1 - -/* Define if you have the 'inet_pton' function. */ -#define HAVE_INET_PTON 1 - -/* Define to 1 if you have the 'initgroups' function. */ -/* #undef HAVE_INITGROUPS */ - -/* Define to 1 if you have the header file. */ -#define HAVE_INTTYPES_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_IO_H */ - -/* Define if gcc has the ipa-pure-const bug. */ -/* #undef HAVE_IPA_PURE_CONST_BUG */ - -/* Define to 1 if you have the 'kill' function. */ -#define HAVE_KILL 1 - -/* Define to 1 if you have the 'killpg' function. */ -#define HAVE_KILLPG 1 - -/* Define if you have the 'kqueue' function. */ -/* #undef HAVE_KQUEUE */ - -/* Define to 1 if you have the header file. */ -#define HAVE_LANGINFO_H 1 - -/* Defined to enable large file support when an off_t is bigger than a long - and long long is at least as big as an off_t. You may need to add some - flags for configuration and compilation to enable this mode. (For Solaris - and Linux, the necessary defines are already defined.) */ -/* #undef HAVE_LARGEFILE_SUPPORT */ - -/* Define to 1 if you have the 'lchflags' function. */ -/* #undef HAVE_LCHFLAGS */ - -/* Define to 1 if you have the 'lchmod' function. */ -/* #undef HAVE_LCHMOD */ - -/* Define to 1 if you have the 'lchown' function. */ -#define HAVE_LCHOWN 1 - -/* Define to 1 if you have the `db' library (-ldb). */ -/* #undef HAVE_LIBDB */ - -/* Define to 1 if you have the 'dl' library (-ldl). */ -#define HAVE_LIBDL 1 - -/* Define to 1 if you have the 'dld' library (-ldld). */ -/* #undef HAVE_LIBDLD */ - -/* Define to 1 if you have the 'ieee' library (-lieee). */ -/* #undef HAVE_LIBIEEE */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBINTL_H */ - -/* Define to 1 if you have the 'sendfile' library (-lsendfile). */ -/* #undef HAVE_LIBSENDFILE */ - -/* Define to 1 if you have the 'sqlite3' library (-lsqlite3). */ -#define HAVE_LIBSQLITE3 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBUTIL_H */ - -/* Define if you have the 'link' function. */ -#define HAVE_LINK 1 - -/* Define to 1 if you have the 'linkat' function. */ -#define HAVE_LINKAT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINK_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_AUXVEC_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_BCM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_J1939_H 1 - -/* Define if compiling using Linux 3.6 or later. */ -#define HAVE_LINUX_CAN_RAW_FD_FRAMES 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_CAN_RAW_H 1 - -/* Define if compiling using Linux 4.1 or later. */ -#define HAVE_LINUX_CAN_RAW_JOIN_FILTERS 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_FS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_LIMITS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_MEMFD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_NETFILTER_IPV4_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_NETLINK_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_QRTR_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_RANDOM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_SCHED_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_SOUNDCARD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_TIPC_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_VM_SOCKETS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LINUX_WAIT_H 1 - -/* Define if you have the 'listen' function. */ -#define HAVE_LISTEN 1 - -/* Define to 1 if you have the 'lockf' function. */ -#define HAVE_LOCKF 1 - -/* Define to 1 if you have the 'log1p' function. */ -#define HAVE_LOG1P 1 - -/* Define to 1 if you have the 'log2' function. */ -#define HAVE_LOG2 1 - -/* Define to 1 if you have the `login_tty' function. */ -#define HAVE_LOGIN_TTY 1 - -/* Define to 1 if the system has the type 'long double'. */ -#define HAVE_LONG_DOUBLE 1 - -/* Define to 1 if you have the 'lstat' function. */ -#define HAVE_LSTAT 1 - -/* Define to 1 if you have the 'lutimes' function. */ -/* #undef HAVE_LUTIMES */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LZMA_H */ - -/* Define to 1 if you have the 'madvise' function. */ -#define HAVE_MADVISE 1 - -/* Define this if you have the makedev macro. */ -#define HAVE_MAKEDEV 1 - -/* Define if you have the 'MAXLOGNAME' constant. */ -/* #undef HAVE_MAXLOGNAME */ - -/* Define to 1 if you have the 'mbrtowc' function. */ -#define HAVE_MBRTOWC 1 - -/* Define if you have the 'memfd_create' function. */ -/* #undef HAVE_MEMFD_CREATE */ - -/* Define to 1 if you have the 'memrchr' function. */ -#define HAVE_MEMRCHR 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_MINIX_CONFIG_H */ - -/* Define to 1 if you have the 'mkdirat' function. */ -#define HAVE_MKDIRAT 1 - -/* Define to 1 if you have the 'mkfifo' function. */ -#define HAVE_MKFIFO 1 - -/* Define to 1 if you have the 'mkfifoat' function. */ -#define HAVE_MKFIFOAT 1 - -/* Define to 1 if you have the 'mknod' function. */ -#define HAVE_MKNOD 1 - -/* Define to 1 if you have the 'mknodat' function. */ -#define HAVE_MKNODAT 1 - -/* Define to 1 if you have the 'mktime' function. */ -#define HAVE_MKTIME 1 - -/* Define to 1 if you have the 'mmap' function. */ -#define HAVE_MMAP 1 - -/* Define to 1 if you have the 'mremap' function. */ -#define HAVE_MREMAP 1 - -/* Define to 1 if you have the 'nanosleep' function. */ -#define HAVE_NANOSLEEP 1 - -/* Define if you have the 'ncurses' library */ -/* #undef HAVE_NCURSES */ - -/* Define if you have the 'ncursesw' library */ -/* #undef HAVE_NCURSESW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_PANEL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_PANEL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NDBM_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. */ -/* #undef HAVE_NDIR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETCAN_CAN_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NETDB_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NETINET_IN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETLINK_NETLINK_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NETPACKET_PACKET_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_ETHERNET_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_IF_H 1 - -/* Define to 1 if you have the 'nice' function. */ -#define HAVE_NICE 1 - -/* Define if the internal form of wchar_t in non-Unicode locales is not - Unicode. */ -/* #undef HAVE_NON_UNICODE_WCHAR_T_REPRESENTATION */ - -/* Define to 1 if you have the 'openat' function. */ -#define HAVE_OPENAT 1 - -/* Define to 1 if you have the 'opendir' function. */ -#define HAVE_OPENDIR 1 - -/* Define to 1 if you have the 'openpty' function. */ -#define HAVE_OPENPTY 1 - -/* Define if you have the 'panel' library */ -/* #undef HAVE_PANEL */ - -/* Define if you have the 'panelw' library */ -/* #undef HAVE_PANELW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PANEL_H */ - -/* Define to 1 if you have the 'pathconf' function. */ -#define HAVE_PATHCONF 1 - -/* Define to 1 if you have the 'pause' function. */ -#define HAVE_PAUSE 1 - -/* Define to 1 if you have the 'pipe' function. */ -#define HAVE_PIPE 1 - -/* Define to 1 if you have the 'pipe2' function. */ -#define HAVE_PIPE2 1 - -/* Define to 1 if you have the 'plock' function. */ -/* #undef HAVE_PLOCK */ - -/* Define to 1 if you have the 'poll' function. */ -#define HAVE_POLL 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_POLL_H 1 - -/* Define to 1 if you have the 'posix_fadvise' function. */ -#define HAVE_POSIX_FADVISE 1 - -/* Define to 1 if you have the 'posix_fallocate' function. */ -#define HAVE_POSIX_FALLOCATE 1 - -/* Define to 1 if you have the 'posix_openpt' function. */ -#define HAVE_POSIX_OPENPT 1 - -/* Define to 1 if you have the 'posix_spawn' function. */ -/* #undef HAVE_POSIX_SPAWN */ - -/* Define to 1 if you have the 'posix_spawnp' function. */ -/* #undef HAVE_POSIX_SPAWNP */ - -/* Define to 1 if you have the 'posix_spawn_file_actions_addclosefrom_np' - function. */ -/* #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP */ - -/* Define to 1 if you have the 'pread' function. */ -#define HAVE_PREAD 1 - -/* Define to 1 if you have the 'preadv' function. */ -#define HAVE_PREADV 1 - -/* Define to 1 if you have the 'preadv2' function. */ -/* #undef HAVE_PREADV2 */ - -/* Define if you have the 'prlimit' function. */ -#define HAVE_PRLIMIT 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PROCESS_H */ - -/* Define to 1 if you have the 'process_vm_readv' function. */ -#define HAVE_PROCESS_VM_READV 1 - -/* Define if your compiler supports function prototype */ -#define HAVE_PROTOTYPES 1 - -/* Define to 1 if you have the 'pthread_condattr_setclock' function. */ -#define HAVE_PTHREAD_CONDATTR_SETCLOCK 1 - -/* Define to 1 if you have the 'pthread_cond_timedwait_relative_np' function. - */ -/* #undef HAVE_PTHREAD_COND_TIMEDWAIT_RELATIVE_NP */ - -/* Defined for Solaris 2.6 bug in pthread header. */ -/* #undef HAVE_PTHREAD_DESTRUCTOR */ - -/* Define to 1 if you have the 'pthread_getattr_np' function. */ -#define HAVE_PTHREAD_GETATTR_NP 1 - -/* Define to 1 if you have the 'pthread_getcpuclockid' function. */ -#define HAVE_PTHREAD_GETCPUCLOCKID 1 - -/* Define to 1 if you have the 'pthread_getname_np' function. */ -/* #undef HAVE_PTHREAD_GETNAME_NP */ - -/* Define to 1 if you have the 'pthread_get_name_np' function. */ -/* #undef HAVE_PTHREAD_GET_NAME_NP */ - -/* Define to 1 if you have the header file. */ -#define HAVE_PTHREAD_H 1 - -/* Define to 1 if you have the 'pthread_init' function. */ -/* #undef HAVE_PTHREAD_INIT */ - -/* Define to 1 if you have the 'pthread_kill' function. */ -#define HAVE_PTHREAD_KILL 1 - -/* Define to 1 if you have the 'pthread_setname_np' function. */ -#define HAVE_PTHREAD_SETNAME_NP 1 - -/* Define to 1 if you have the 'pthread_set_name_np' function. */ -/* #undef HAVE_PTHREAD_SET_NAME_NP */ - -/* Define to 1 if you have the 'pthread_sigmask' function. */ -#define HAVE_PTHREAD_SIGMASK 1 - -/* Define if platform requires stubbed pthreads support */ -/* #undef HAVE_PTHREAD_STUBS */ - -/* Define to 1 if you have the 'ptsname' function. */ -#define HAVE_PTSNAME 1 - -/* Define to 1 if you have the 'ptsname_r' function. */ -#define HAVE_PTSNAME_R 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_PTY_H 1 - -/* Define to 1 if you have the 'pwrite' function. */ -#define HAVE_PWRITE 1 - -/* Define to 1 if you have the 'pwritev' function. */ -#define HAVE_PWRITEV 1 - -/* Define to 1 if you have the 'pwritev2' function. */ -/* #undef HAVE_PWRITEV2 */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_READLINE_READLINE_H */ - -/* Define to 1 if you have the 'readlink' function. */ -#define HAVE_READLINK 1 - -/* Define to 1 if you have the 'readlinkat' function. */ -#define HAVE_READLINKAT 1 - -/* Define to 1 if you have the 'readv' function. */ -#define HAVE_READV 1 - -/* Define to 1 if you have the 'realpath' function. */ -#define HAVE_REALPATH 1 - -/* Define if you have the 'recvfrom' function. */ -#define HAVE_RECVFROM 1 - -/* Define to 1 if you have the 'renameat' function. */ -#define HAVE_RENAMEAT 1 - -/* Define if readline supports append_history */ -/* #undef HAVE_RL_APPEND_HISTORY */ - -/* Define if you can turn off readline's signal handling. */ -/* #undef HAVE_RL_CATCH_SIGNAL */ - -/* Define to 1 if the system has the type 'rl_compdisp_func_t'. */ -/* #undef HAVE_RL_COMPDISP_FUNC_T */ - -/* Define if you have readline 2.2 */ -/* #undef HAVE_RL_COMPLETION_APPEND_CHARACTER */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_COMPLETION_DISPLAY_MATCHES_HOOK */ - -/* Define if you have readline 4.2 */ -/* #undef HAVE_RL_COMPLETION_MATCHES */ - -/* Define if you have rl_completion_suppress_append */ -/* #undef HAVE_RL_COMPLETION_SUPPRESS_APPEND */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_PRE_INPUT_HOOK */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_RESIZE_TERMINAL */ - -/* Define to 1 if you have the 'rtpSpawn' function. */ -/* #undef HAVE_RTPSPAWN */ - -/* Define to 1 if you have the 'sched_get_priority_max' function. */ -#define HAVE_SCHED_GET_PRIORITY_MAX 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SCHED_H 1 - -/* Define to 1 if you have the 'sched_rr_get_interval' function. */ -#define HAVE_SCHED_RR_GET_INTERVAL 1 - -/* Define to 1 if you have the 'sched_setaffinity' function. */ -#define HAVE_SCHED_SETAFFINITY 1 - -/* Define to 1 if you have the 'sched_setparam' function. */ -#define HAVE_SCHED_SETPARAM 1 - -/* Define to 1 if you have the 'sched_setscheduler' function. */ -#define HAVE_SCHED_SETSCHEDULER 1 - -/* Define to 1 if you have the 'sem_clockwait' function. */ -/* #undef HAVE_SEM_CLOCKWAIT */ - -/* Define to 1 if you have the 'sem_getvalue' function. */ -#define HAVE_SEM_GETVALUE 1 - -/* Define to 1 if you have the 'sem_open' function. */ -/* #undef HAVE_SEM_OPEN */ - -/* Define to 1 if you have the 'sem_timedwait' function. */ -#define HAVE_SEM_TIMEDWAIT 1 - -/* Define to 1 if you have the 'sem_unlink' function. */ -/* #undef HAVE_SEM_UNLINK */ - -/* Define to 1 if you have the 'sendfile' function. */ -#define HAVE_SENDFILE 1 - -/* Define if you have the 'sendto' function. */ -#define HAVE_SENDTO 1 - -/* Define to 1 if you have the 'setegid' function. */ -/* #undef HAVE_SETEGID */ - -/* Define to 1 if you have the 'seteuid' function. */ -/* #undef HAVE_SETEUID */ - -/* Define to 1 if you have the 'setgid' function. */ -/* #undef HAVE_SETGID */ - -/* Define if you have the 'setgroups' function. */ -#define HAVE_SETGROUPS 1 - -/* Define to 1 if you have the 'sethostname' function. */ -/* #undef HAVE_SETHOSTNAME */ - -/* Define to 1 if you have the 'setitimer' function. */ -#define HAVE_SETITIMER 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SETJMP_H 1 - -/* Define to 1 if you have the 'setlocale' function. */ -#define HAVE_SETLOCALE 1 - -/* Define to 1 if you have the 'setns' function. */ -#define HAVE_SETNS 1 - -/* Define to 1 if you have the 'setpgid' function. */ -#define HAVE_SETPGID 1 - -/* Define to 1 if you have the 'setpgrp' function. */ -#define HAVE_SETPGRP 1 - -/* Define to 1 if you have the 'setpriority' function. */ -#define HAVE_SETPRIORITY 1 - -/* Define to 1 if you have the 'setregid' function. */ -/* #undef HAVE_SETREGID */ - -/* Define to 1 if you have the 'setresgid' function. */ -/* #undef HAVE_SETRESGID */ - -/* Define to 1 if you have the 'setresuid' function. */ -/* #undef HAVE_SETRESUID */ - -/* Define to 1 if you have the 'setreuid' function. */ -/* #undef HAVE_SETREUID */ - -/* Define to 1 if you have the 'setsid' function. */ -#define HAVE_SETSID 1 - -/* Define if you have the 'setsockopt' function. */ -#define HAVE_SETSOCKOPT 1 - -/* Define to 1 if you have the 'setuid' function. */ -/* #undef HAVE_SETUID */ - -/* Define to 1 if you have the 'setvbuf' function. */ -#define HAVE_SETVBUF 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SHADOW_H */ - -/* Define to 1 if you have the 'shm_open' function. */ -/* #undef HAVE_SHM_OPEN */ - -/* Define to 1 if you have the 'shm_unlink' function. */ -/* #undef HAVE_SHM_UNLINK */ - -/* Define to 1 if you have the 'shutdown' function. */ -#define HAVE_SHUTDOWN 1 - -/* Define to 1 if you have the 'sigaction' function. */ -#define HAVE_SIGACTION 1 - -/* Define to 1 if you have the 'sigaltstack' function. */ -#define HAVE_SIGALTSTACK 1 - -/* Define to 1 if you have the 'sigfillset' function. */ -#define HAVE_SIGFILLSET 1 - -/* Define to 1 if 'si_band' is a member of 'siginfo_t'. */ -#define HAVE_SIGINFO_T_SI_BAND 1 - -/* Define to 1 if you have the 'siginterrupt' function. */ -#define HAVE_SIGINTERRUPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SIGNAL_H 1 - -/* Define to 1 if you have the 'sigpending' function. */ -#define HAVE_SIGPENDING 1 - -/* Define to 1 if you have the 'sigrelse' function. */ -/* #undef HAVE_SIGRELSE */ - -/* Define to 1 if you have the 'sigtimedwait' function. */ -#define HAVE_SIGTIMEDWAIT 1 - -/* Define to 1 if you have the 'sigwait' function. */ -#define HAVE_SIGWAIT 1 - -/* Define to 1 if you have the 'sigwaitinfo' function. */ -#define HAVE_SIGWAITINFO 1 - -/* Define to 1 if you have the 'snprintf' function. */ -#define HAVE_SNPRINTF 1 - -/* struct sockaddr_alg (linux/if_alg.h) */ -#define HAVE_SOCKADDR_ALG 1 - -/* Define if sockaddr has sa_len member */ -/* #undef HAVE_SOCKADDR_SA_LEN */ - -/* struct sockaddr_storage (sys/socket.h) */ -#define HAVE_SOCKADDR_STORAGE 1 - -/* Define if you have the 'socket' function. */ -#define HAVE_SOCKET 1 - -/* Define if you have the 'socketpair' function. */ -#define HAVE_SOCKETPAIR 1 - -/* Define to 1 if the system has the type 'socklen_t'. */ -#define HAVE_SOCKLEN_T 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SPAWN_H 1 - -/* Define to 1 if you have the 'splice' function. */ -#define HAVE_SPLICE 1 - -/* Define to 1 if the system has the type 'ssize_t'. */ -#define HAVE_SSIZE_T 1 - -/* Define to 1 if you have the 'statvfs' function. */ -#define HAVE_STATVFS 1 - -/* Define if you have struct stat.st_mtim.tv_nsec */ -#define HAVE_STAT_TV_NSEC 1 - -/* Define if you have struct stat.st_mtimensec */ -/* #undef HAVE_STAT_TV_NSEC2 */ - -/* Define to 1 if you have the header file. */ -#define HAVE_STDINT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDLIB_H 1 - -/* Has stdatomic.h with atomic_int and atomic_uintptr_t */ -#define HAVE_STD_ATOMIC 1 - -/* Define to 1 if you have the 'strftime' function. */ -#define HAVE_STRFTIME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRINGS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRING_H 1 - -/* Define to 1 if you have the 'strlcpy' function. */ -#define HAVE_STRLCPY 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_STROPTS_H */ - -/* Define to 1 if you have the 'strsignal' function. */ -#define HAVE_STRSIGNAL 1 - -/* Define to 1 if 'pw_gecos' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_GECOS 1 - -/* Define to 1 if 'pw_passwd' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_PASSWD 1 - -/* Define to 1 if 'st_birthtime' is a member of 'struct stat'. */ -/* #undef HAVE_STRUCT_STAT_ST_BIRTHTIME */ - -/* Define to 1 if 'st_blksize' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLKSIZE 1 - -/* Define to 1 if 'st_blocks' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLOCKS 1 - -/* Define to 1 if 'st_flags' is a member of 'struct stat'. */ -/* #undef HAVE_STRUCT_STAT_ST_FLAGS */ - -/* Define to 1 if 'st_gen' is a member of 'struct stat'. */ -/* #undef HAVE_STRUCT_STAT_ST_GEN */ - -/* Define to 1 if 'st_rdev' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_RDEV 1 - -/* Define to 1 if 'tm_zone' is a member of 'struct tm'. */ -#define HAVE_STRUCT_TM_TM_ZONE 1 - -/* Define if you have the 'symlink' function. */ -#define HAVE_SYMLINK 1 - -/* Define to 1 if you have the 'symlinkat' function. */ -#define HAVE_SYMLINKAT 1 - -/* Define to 1 if you have the 'sync' function. */ -#define HAVE_SYNC 1 - -/* Define to 1 if you have the 'sysconf' function. */ -#define HAVE_SYSCONF 1 - -/* Define to 1 if you have the 'sysctlbyname' function. */ -/* #undef HAVE_SYSCTLBYNAME */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSEXITS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSLOG_H 1 - -/* Define to 1 if you have the 'system' function. */ -#define HAVE_SYSTEM 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_AUDIOIO_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_AUXV_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_BSDTTY_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_DEVPOLL_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_DIR_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_ENDIAN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_EPOLL_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_EVENTFD_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_EVENT_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_FILE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_IOCTL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_KERN_CONTROL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_LOADAVG_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_LOCK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MEMFD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MKDEV_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_MMAN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MODEM_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_NDIR_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_PARAM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_PIDFD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_POLL_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_RANDOM_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_RESOURCE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SELECT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SENDFILE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SOCKET_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SOUNDCARD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STATVFS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STAT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SYSCALL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYSCTL_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SYSMACROS_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYS_DOMAIN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_TERMIO_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIMERFD_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIMES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TYPES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UTSNAME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_WAIT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_XATTR_H 1 - -/* Define to 1 if you have the 'tcgetpgrp' function. */ -#define HAVE_TCGETPGRP 1 - -/* Define to 1 if you have the 'tcsetpgrp' function. */ -#define HAVE_TCSETPGRP 1 - -/* Define to 1 if you have the 'tempnam' function. */ -#define HAVE_TEMPNAM 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_TERMIOS_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_TERM_H */ - -/* Define to 1 if you have the 'timegm' function. */ -#define HAVE_TIMEGM 1 - -/* Define if you have the 'timerfd_create' function. */ -#define HAVE_TIMERFD_CREATE 1 - -/* Define to 1 if you have the 'times' function. */ -#define HAVE_TIMES 1 - -/* Define to 1 if you have the 'tmpfile' function. */ -#define HAVE_TMPFILE 1 - -/* Define to 1 if you have the 'tmpnam' function. */ -#define HAVE_TMPNAM 1 - -/* Define to 1 if you have the 'tmpnam_r' function. */ -/* #undef HAVE_TMPNAM_R */ - -/* Define to 1 if your 'struct tm' has 'tm_zone'. Deprecated, use - 'HAVE_STRUCT_TM_TM_ZONE' instead. */ -#define HAVE_TM_ZONE 1 - -/* Define to 1 if you have the 'truncate' function. */ -#define HAVE_TRUNCATE 1 - -/* Define to 1 if you have the 'ttyname_r' function. */ -#define HAVE_TTYNAME_R 1 - -/* Define to 1 if you don't have 'tm_zone' but do have the external array - 'tzname'. */ -/* #undef HAVE_TZNAME */ - -/* Define to 1 if you have the 'umask' function. */ -#define HAVE_UMASK 1 - -/* Define to 1 if you have the 'uname' function. */ -#define HAVE_UNAME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UNISTD_H 1 - -/* Define to 1 if you have the 'unlinkat' function. */ -#define HAVE_UNLINKAT 1 - -/* Define to 1 if you have the 'unlockpt' function. */ -#define HAVE_UNLOCKPT 1 - -/* Define to 1 if you have the 'unshare' function. */ -#define HAVE_UNSHARE 1 - -/* Define if you have a useable wchar_t type defined in wchar.h; useable means - wchar_t must be an unsigned type with at least 16 bits. (see - Include/unicodeobject.h). */ -/* #undef HAVE_USABLE_WCHAR_T */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UTIL_H */ - -/* Define to 1 if you have the 'utimensat' function. */ -#define HAVE_UTIMENSAT 1 - -/* Define to 1 if you have the 'utimes' function. */ -#define HAVE_UTIMES 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UTIME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UTMP_H 1 - -/* Define if you have the 'HAVE_UT_NAMESIZE' constant. */ -#define HAVE_UT_NAMESIZE 1 - -/* Define to 1 if you have the 'uuid_create' function. */ -/* #undef HAVE_UUID_CREATE */ - -/* Define to 1 if you have the 'uuid_enc_be' function. */ -/* #undef HAVE_UUID_ENC_BE */ - -/* Define if uuid_generate_time_safe() exists. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE */ - -/* Define if uuid_generate_time_safe() is able to deduce a MAC address. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE_STABLE_MAC */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UUID_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UUID_UUID_H */ - -/* Define to 1 if you have the 'vfork' function. */ -#define HAVE_VFORK 1 - -/* Define to 1 if you have the 'wait' function. */ -#define HAVE_WAIT 1 - -/* Define to 1 if you have the 'wait3' function. */ -/* #undef HAVE_WAIT3 */ - -/* Define to 1 if you have the 'wait4' function. */ -#define HAVE_WAIT4 1 - -/* Define to 1 if you have the 'waitid' function. */ -#define HAVE_WAITID 1 - -/* Define to 1 if you have the 'waitpid' function. */ -#define HAVE_WAITPID 1 - -/* Define if the compiler provides a wchar.h header file. */ -#define HAVE_WCHAR_H 1 - -/* Define to 1 if you have the 'wcscoll' function. */ -#define HAVE_WCSCOLL 1 - -/* Define to 1 if you have the 'wcsftime' function. */ -#define HAVE_WCSFTIME 1 - -/* Define to 1 if you have the 'wcsxfrm' function. */ -#define HAVE_WCSXFRM 1 - -/* Define to 1 if you have the 'wmemcmp' function. */ -#define HAVE_WMEMCMP 1 - -/* Define if tzset() actually switches the local timezone in a meaningful way. - */ -/* #undef HAVE_WORKING_TZSET */ - -/* Define to 1 if you have the 'writev' function. */ -#define HAVE_WRITEV 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZDICT_H */ - -/* Define if the zlib library has inflateCopy */ -#define HAVE_ZLIB_COPY 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ZLIB_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZSTD_H */ - -/* Define to 1 if you have the '_getpty' function. */ -/* #undef HAVE__GETPTY */ - -/* Define to 1 if the system has the type '__uint128_t'. */ -#define HAVE___UINT128_T 1 - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in . - */ -/* #undef MAJOR_IN_MKDEV */ - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in - . */ -#define MAJOR_IN_SYSMACROS 1 - -/* Define if mvwdelch in curses.h is an expression. */ -/* #undef MVWDELCH_IS_EXPRESSION */ - -/* Define to the address where bug reports for this package should be sent. */ -/* #undef PACKAGE_BUGREPORT */ - -/* Define to the full name of this package. */ -/* #undef PACKAGE_NAME */ - -/* Define to the full name and version of this package. */ -/* #undef PACKAGE_STRING */ - -/* Define to the one symbol short name of this package. */ -/* #undef PACKAGE_TARNAME */ - -/* Define to the home page for this package. */ -/* #undef PACKAGE_URL */ - -/* Define to the version of this package. */ -/* #undef PACKAGE_VERSION */ - -/* Define if POSIX semaphores aren't enabled on your system */ -/* #undef POSIX_SEMAPHORES_NOT_ENABLED */ - -/* Define if pthread_key_t is compatible with int. */ -#define PTHREAD_KEY_T_IS_COMPATIBLE_WITH_INT 1 - -/* Defined if PTHREAD_SCOPE_SYSTEM supported. */ -/* #undef PTHREAD_SYSTEM_SCHED_SUPPORTED */ - -/* Define as the preferred size in bits of long digits */ -/* #undef PYLONG_BITS_IN_DIGIT */ - -/* enabled builtin hash modules */ -#define PY_BUILTIN_HASHLIB_HASHES "md5,sha1,sha2,sha3,blake2" - -/* Define if you want to coerce the C locale to a UTF-8 based locale */ -#define PY_COERCE_C_LOCALE 1 - -/* Define to 1 if you have the perf trampoline. */ -/* #undef PY_HAVE_PERF_TRAMPOLINE */ - -/* Define to 1 to build the sqlite module with loadable extensions support. */ -/* #undef PY_SQLITE_ENABLE_LOAD_EXTENSION */ - -/* Define if SQLite was compiled with the serialize API */ -#define PY_SQLITE_HAVE_SERIALIZE 1 - -/* Default cipher suites list for ssl module. 1: Python's preferred selection, - 2: leave OpenSSL defaults untouched, 0: custom string */ -#define PY_SSL_DEFAULT_CIPHERS 1 - -/* Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0 */ -/* #undef PY_SSL_DEFAULT_CIPHER_STRING */ - -/* PEP 11 Support tier (1, 2, 3 or 0 for unsupported) */ -#define PY_SUPPORT_TIER 3 - -/* Define if you want to build an interpreter with many run-time checks. */ -/* #undef Py_DEBUG */ - -/* Defined if Python is built as a shared library. */ -#define Py_ENABLE_SHARED 1 - -/* Define if you want to disable the GIL */ -/* #undef Py_GIL_DISABLED */ - -/* Define hash algorithm for str, bytes and memoryview. SipHash24: 1, FNV: 2, - SipHash13: 3, externally defined: 0 */ -/* #undef Py_HASH_ALGORITHM */ - -/* Define if you want to enable remote debugging support. */ -#define Py_REMOTE_DEBUG 1 - -/* Define if rl_startup_hook takes arguments */ -/* #undef Py_RL_STARTUP_HOOK_TAKES_ARGS */ - -/* Define if you want to enable internal statistics gathering. */ -/* #undef Py_STATS */ - -/* The version of SunOS/Solaris as reported by `uname -r' without the dot. */ -/* #undef Py_SUNOS_VERSION */ - -/* Define if you want to use tail-calling interpreters in CPython. */ -/* #undef Py_TAIL_CALL_INTERP */ - -/* Define if you want to enable tracing references for debugging purpose */ -/* #undef Py_TRACE_REFS */ - -/* assume C89 semantics that RETSIGTYPE is always void */ -#define RETSIGTYPE void - -/* Define if setpgrp() must be called as setpgrp(0, 0). */ -/* #undef SETPGRP_HAVE_ARG */ - -/* Define if i>>j for signed int i does not extend the sign bit when i < 0 */ -/* #undef SIGNED_RIGHT_SHIFT_ZERO_FILLS */ - -/* The size of 'double', as computed by sizeof. */ -#define SIZEOF_DOUBLE 8 - -/* The size of 'float', as computed by sizeof. */ -#define SIZEOF_FLOAT 4 - -/* The size of 'fpos_t', as computed by sizeof. */ -#define SIZEOF_FPOS_T 8 - -/* The size of 'int', as computed by sizeof. */ -#define SIZEOF_INT 4 - -/* The size of 'long', as computed by sizeof. */ -#define SIZEOF_LONG 8 - -/* The size of 'long double', as computed by sizeof. */ -#define SIZEOF_LONG_DOUBLE 16 - -/* The size of 'long long', as computed by sizeof. */ -#define SIZEOF_LONG_LONG 8 - -/* The size of 'off_t', as computed by sizeof. */ -#define SIZEOF_OFF_T 8 - -/* The size of 'pid_t', as computed by sizeof. */ -#define SIZEOF_PID_T 4 - -/* The size of 'pthread_key_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_KEY_T 4 - -/* The size of 'pthread_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_T 8 - -/* The size of 'short', as computed by sizeof. */ -#define SIZEOF_SHORT 2 - -/* The size of 'size_t', as computed by sizeof. */ -#define SIZEOF_SIZE_T 8 - -/* The size of 'time_t', as computed by sizeof. */ -#define SIZEOF_TIME_T 8 - -/* The size of 'uintptr_t', as computed by sizeof. */ -#define SIZEOF_UINTPTR_T 8 - -/* The size of 'void *', as computed by sizeof. */ -#define SIZEOF_VOID_P 8 - -/* The size of 'wchar_t', as computed by sizeof. */ -#define SIZEOF_WCHAR_T 4 - -/* The size of '_Bool', as computed by sizeof. */ -#define SIZEOF__BOOL 1 - -/* Define to 1 if you have the ANSI C header files. */ -#define STDC_HEADERS 1 - -/* Define if you can safely include both and - (which you can't on SCO ODT 3.0). */ -#define SYS_SELECT_WITH_SYS_TIME 1 - -/* Custom thread stack size depending on chosen sanitizer runtimes. */ -/* #undef THREAD_STACK_SIZE */ - -/* Library needed by timemodule.c: librt may be needed for clock_gettime() */ -/* #undef TIMEMODULE_LIB */ - -/* Define to 1 if your declares 'struct tm'. */ -/* #undef TM_IN_SYS_TIME */ - -/* Define if you want to use computed gotos in ceval.c. */ -/* #undef USE_COMPUTED_GOTOS */ - -/* Enable extensions on AIX, Interix, z/OS. */ -#ifndef _ALL_SOURCE -# define _ALL_SOURCE 1 -#endif -/* Enable general extensions on macOS. */ -#ifndef _DARWIN_C_SOURCE -# define _DARWIN_C_SOURCE 1 -#endif -/* Enable general extensions on Solaris. */ -#ifndef __EXTENSIONS__ -# define __EXTENSIONS__ 1 -#endif -/* Enable GNU extensions on systems that have them. */ -#ifndef _GNU_SOURCE -# define _GNU_SOURCE 1 -#endif -/* Enable X/Open compliant socket functions that do not require linking - with -lxnet on HP-UX 11.11. */ -#ifndef _HPUX_ALT_XOPEN_SOCKET_API -# define _HPUX_ALT_XOPEN_SOCKET_API 1 -#endif -/* Identify the host operating system as Minix. - This macro does not affect the system headers' behavior. - A future release of Autoconf may stop defining this macro. */ -#ifndef _MINIX -/* # undef _MINIX */ -#endif -/* Enable general extensions on NetBSD. - Enable NetBSD compatibility extensions on Minix. */ -#ifndef _NETBSD_SOURCE -# define _NETBSD_SOURCE 1 -#endif -/* Enable OpenBSD compatibility extensions on NetBSD. - Oddly enough, this does nothing on OpenBSD. */ -#ifndef _OPENBSD_SOURCE -# define _OPENBSD_SOURCE 1 -#endif -/* Define to 1 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_SOURCE -/* # undef _POSIX_SOURCE */ -#endif -/* Define to 2 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_1_SOURCE -/* # undef _POSIX_1_SOURCE */ -#endif -/* Enable POSIX-compatible threading on Solaris. */ -#ifndef _POSIX_PTHREAD_SEMANTICS -# define _POSIX_PTHREAD_SEMANTICS 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-5:2014. */ -#ifndef __STDC_WANT_IEC_60559_ATTRIBS_EXT__ -# define __STDC_WANT_IEC_60559_ATTRIBS_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-1:2014. */ -#ifndef __STDC_WANT_IEC_60559_BFP_EXT__ -# define __STDC_WANT_IEC_60559_BFP_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-2:2015. */ -#ifndef __STDC_WANT_IEC_60559_DFP_EXT__ -# define __STDC_WANT_IEC_60559_DFP_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex F. */ -#ifndef __STDC_WANT_IEC_60559_EXT__ -# define __STDC_WANT_IEC_60559_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-4:2015. */ -#ifndef __STDC_WANT_IEC_60559_FUNCS_EXT__ -# define __STDC_WANT_IEC_60559_FUNCS_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex H and ISO/IEC TS 18661-3:2015. */ -#ifndef __STDC_WANT_IEC_60559_TYPES_EXT__ -# define __STDC_WANT_IEC_60559_TYPES_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TR 24731-2:2010. */ -#ifndef __STDC_WANT_LIB_EXT2__ -# define __STDC_WANT_LIB_EXT2__ 1 -#endif -/* Enable extensions specified by ISO/IEC 24747:2009. */ -#ifndef __STDC_WANT_MATH_SPEC_FUNCS__ -# define __STDC_WANT_MATH_SPEC_FUNCS__ 1 -#endif -/* Enable extensions on HP NonStop. */ -#ifndef _TANDEM_SOURCE -# define _TANDEM_SOURCE 1 -#endif -/* Enable X/Open extensions. Define to 500 only if necessary - to make mbstate_t available. */ -#ifndef _XOPEN_SOURCE -# define _XOPEN_SOURCE 700 -#endif - - -/* Define if WINDOW in curses.h offers a field _flags. */ -/* #undef WINDOW_HAS_FLAGS */ - -/* Define if you want build the _decimal module using a coroutine-local rather - than a thread-local context */ -#define WITH_DECIMAL_CONTEXTVAR 1 - -/* Define if you want documentation strings in extension modules */ -#define WITH_DOC_STRINGS 1 - -/* Define if you want to compile in DTrace support */ -/* #undef WITH_DTRACE */ - -/* Define if you want to use the new-style (Openstep, Rhapsody, MacOS) dynamic - linker (dyld) instead of the old-style (NextStep) dynamic linker (rld). - Dyld is necessary to support frameworks. */ -/* #undef WITH_DYLD */ - -/* Define to build the readline module against libedit. */ -/* #undef WITH_EDITLINE */ - -/* Define to 1 if libintl is needed for locale functions. */ -/* #undef WITH_LIBINTL */ - -/* Define if you want to compile in mimalloc memory allocator. */ -#define WITH_MIMALLOC 1 - -/* Define if you want to produce an OpenStep/Rhapsody framework (shared - library plus accessory files). */ -/* #undef WITH_NEXT_FRAMEWORK */ - -/* Define if you want to compile in Python-specific mallocs */ -#define WITH_PYMALLOC 1 - -/* Define if you want pymalloc to be disabled when running under valgrind */ -/* #undef WITH_VALGRIND */ - -/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most - significant byte first (like Motorola and SPARC, unlike Intel). */ -#if defined AC_APPLE_UNIVERSAL_BUILD -# if defined __BIG_ENDIAN__ -# define WORDS_BIGENDIAN 1 -# endif -#else -# ifndef WORDS_BIGENDIAN -/* # undef WORDS_BIGENDIAN */ -# endif -#endif - -/* Define if arithmetic is subject to x87-style double rounding issue */ -/* #undef X87_DOUBLE_ROUNDING */ - -/* Define on OpenBSD to activate all library features */ -/* #undef _BSD_SOURCE */ - -/* Define on Darwin to activate all library features */ -#define _DARWIN_C_SOURCE 1 - -/* This must be set to 64 on some systems to enable large file support. */ -#define _FILE_OFFSET_BITS 64 - -/* Define to include mbstate_t for mbrtowc */ -/* #undef _INCLUDE__STDC_A1_SOURCE */ - -/* This must be defined on some systems to enable large file support. */ -#define _LARGEFILE_SOURCE 1 - -/* This must be defined on AIX systems to enable large file support. */ -/* #undef _LARGE_FILES */ - -/* Define on NetBSD to activate all library features */ -#define _NETBSD_SOURCE 1 - -/* Define to activate features from IEEE Stds 1003.1-2008 */ -#define _POSIX_C_SOURCE 200809L - -/* Define if you have POSIX threads, and your system does not define that. */ -/* #undef _POSIX_THREADS */ - -/* framework name */ -#define _PYTHONFRAMEWORK "" - -/* Maximum length in bytes of a thread name */ -#define _PYTHREAD_NAME_MAXLEN 15 - -/* Defined if _Complex C type can be used with libffi. */ -/* #undef _Py_FFI_SUPPORT_C_COMPLEX */ - -/* HACL* library can compile SIMD128 implementations */ -/* #undef _Py_HACL_CAN_COMPILE_VEC128 */ - -/* HACL* library can compile SIMD256 implementations */ -/* #undef _Py_HACL_CAN_COMPILE_VEC256 */ - -/* Thread stack size set by the linker (in bytes). */ -/* #undef _Py_LINKER_THREAD_STACK_SIZE */ - -/* Define to 1 if the machine stack grows down (default); 0 if it grows up. */ -#define _Py_STACK_GROWS_DOWN 1 - -/* Define to force use of thread-safe errno, h_errno, and other functions */ -#define _REENTRANT 1 - -/* Define to 1 if you want to emulate getpid() on WASI */ -/* #undef _WASI_EMULATED_GETPID */ - -/* Define to 1 if you want to emulate process clocks on WASI */ -/* #undef _WASI_EMULATED_PROCESS_CLOCKS */ - -/* Define to 1 if you want to emulate signals on WASI */ -/* #undef _WASI_EMULATED_SIGNAL */ - -/* Define to the level of X/Open that your system supports */ -#define _XOPEN_SOURCE 700 - -/* Define to activate Unix95-and-earlier features */ -#define _XOPEN_SOURCE_EXTENDED 1 - -/* Define on FreeBSD to activate all library features */ -#define __BSD_VISIBLE 1 - -/* Define to 'long' if does not define clock_t. */ -/* #undef clock_t */ - -/* Define to empty if 'const' does not conform to ANSI C. */ -/* #undef const */ - -/* Define as 'int' if doesn't define. */ -/* #undef gid_t */ - -/* Define to 'int' if does not define. */ -/* #undef mode_t */ - -/* Define to 'long int' if does not define. */ -/* #undef off_t */ - -/* Define as a signed integer type capable of holding a process identifier. */ -/* #undef pid_t */ - -/* Define to empty if the keyword does not work. */ -/* #undef signed */ - -/* Define as 'unsigned int' if doesn't define. */ -/* #undef size_t */ - -/* Define to 'int' if does not define. */ -/* #undef socklen_t */ - -/* Define as 'int' if doesn't define. */ -/* #undef uid_t */ - - -/* Define the macros needed if on a UnixWare 7.x system. */ -#if defined(__USLC__) && defined(__SCO_VERSION__) -#define STRICT_SYSV_CURSES /* Don't use ncurses extensions */ -#endif - -#endif /*Py_PYCONFIG_H*/ - diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-ios-arm64.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-ios-arm64.h deleted file mode 100644 index 5cdb52f8c..000000000 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-ios-arm64.h +++ /dev/null @@ -1,2088 +0,0 @@ -/* pyconfig.h. Generated from pyconfig.h.in by configure. */ -/* pyconfig.h.in. Generated from configure.ac by autoheader. */ - - -#ifndef Py_PYCONFIG_H -#define Py_PYCONFIG_H - - -/* Define if building universal (internal helper macro) */ -/* #undef AC_APPLE_UNIVERSAL_BUILD */ - -/* BUILD_GNU_TYPE + AIX_BUILDDATE are used to construct the PEP425 tag of the - build system. */ -/* #undef AIX_BUILDDATE */ - -/* Define for AIX if your compiler is a genuine IBM xlC/xlC_r and you want - support for AIX C++ shared extension modules. */ -/* #undef AIX_GENUINE_CPLUSPLUS */ - -/* The normal alignment of 'long', in bytes. */ -#define ALIGNOF_LONG 8 - -/* The normal alignment of 'max_align_t', in bytes. */ -#define ALIGNOF_MAX_ALIGN_T 8 - -/* The normal alignment of 'size_t', in bytes. */ -#define ALIGNOF_SIZE_T 8 - -/* Alternative SOABI used in debug build to load C extensions built in release - mode */ -/* #undef ALT_SOABI */ - -/* The Android API level. */ -/* #undef ANDROID_API_LEVEL */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored in ARM - mixed-endian order (byte order 45670123) */ -/* #undef DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the most - significant byte first */ -/* #undef DOUBLE_IS_BIG_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the - least significant byte first */ -#define DOUBLE_IS_LITTLE_ENDIAN_IEEE754 1 - -/* Define if --enable-ipv6 is specified */ -#define ENABLE_IPV6 1 - -/* Define if getpgrp() must be called as getpgrp(0). */ -/* #undef GETPGRP_HAVE_ARG */ - -/* Define if you have the 'accept' function. */ -#define HAVE_ACCEPT 1 - -/* Define to 1 if you have the 'accept4' function. */ -/* #undef HAVE_ACCEPT4 */ - -/* Define to 1 if you have the 'acosh' function. */ -#define HAVE_ACOSH 1 - -/* struct addrinfo (netdb.h) */ -#define HAVE_ADDRINFO 1 - -/* Define to 1 if you have the 'alarm' function. */ -#define HAVE_ALARM 1 - -/* Define if aligned memory access is required */ -#define HAVE_ALIGNED_REQUIRED 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ALLOCA_H 1 - -/* Define this if your time.h defines altzone. */ -/* #undef HAVE_ALTZONE */ - -/* Define to 1 if you have the 'asinh' function. */ -#define HAVE_ASINH 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ASM_TYPES_H */ - -/* Define to 1 if you have the 'atanh' function. */ -#define HAVE_ATANH 1 - -/* Define to 1 if you have the 'backtrace' function. */ -#define HAVE_BACKTRACE 1 - -/* Define if you have the 'bind' function. */ -#define HAVE_BIND 1 - -/* Define to 1 if you have the 'bind_textdomain_codeset' function. */ -/* #undef HAVE_BIND_TEXTDOMAIN_CODESET */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_BLUETOOTH_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_H */ - -/* Define if mbstowcs(NULL, "text", 0) does not return the number of wide - chars that would be converted. */ -/* #undef HAVE_BROKEN_MBSTOWCS */ - -/* Define if nice() returns success/failure instead of the new priority. */ -/* #undef HAVE_BROKEN_NICE */ - -/* Define if the system reports an invalid PIPE_BUF value. */ -/* #undef HAVE_BROKEN_PIPE_BUF */ - -/* Define if poll() sets errno on invalid file descriptors. */ -/* #undef HAVE_BROKEN_POLL */ - -/* Define if the Posix semaphores do not work on your system */ -/* #undef HAVE_BROKEN_POSIX_SEMAPHORES */ - -/* Define if pthread_sigmask() does not work on your system. */ -/* #undef HAVE_BROKEN_PTHREAD_SIGMASK */ - -/* define to 1 if your sem_getvalue is broken. */ -#define HAVE_BROKEN_SEM_GETVALUE 1 - -/* Define if 'unsetenv' does not return an int. */ -/* #undef HAVE_BROKEN_UNSETENV */ - -/* Has builtin __atomic_load_n() and __atomic_store_n() functions */ -#define HAVE_BUILTIN_ATOMIC 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BZLIB_H */ - -/* Define to 1 if you have the 'chflags' function. */ -#define HAVE_CHFLAGS 1 - -/* Define to 1 if you have the 'chmod' function. */ -#define HAVE_CHMOD 1 - -/* Define to 1 if you have the 'chown' function. */ -#define HAVE_CHOWN 1 - -/* Define if you have the 'chroot' function. */ -#define HAVE_CHROOT 1 - -/* Define to 1 if you have the 'clock' function. */ -#define HAVE_CLOCK 1 - -/* Define to 1 if you have the 'clock_getres' function. */ -#define HAVE_CLOCK_GETRES 1 - -/* Define to 1 if you have the 'clock_gettime' function. */ -#define HAVE_CLOCK_GETTIME 1 - -/* Define to 1 if you have the 'clock_nanosleep' function. */ -/* #undef HAVE_CLOCK_NANOSLEEP */ - -/* Define to 1 if you have the 'clock_settime' function. */ -/* #undef HAVE_CLOCK_SETTIME */ - -/* Define to 1 if the system has the type 'clock_t'. */ -#define HAVE_CLOCK_T 1 - -/* Define to 1 if you have the 'closefrom' function. */ -/* #undef HAVE_CLOSEFROM */ - -/* Define to 1 if you have the 'close_range' function. */ -/* #undef HAVE_CLOSE_RANGE */ - -/* Define if the C compiler supports computed gotos. */ -/* #undef HAVE_COMPUTED_GOTOS */ - -/* Define to 1 if you have the 'confstr' function. */ -#define HAVE_CONFSTR 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CONIO_H */ - -/* Define if you have the 'connect' function. */ -#define HAVE_CONNECT 1 - -/* Define to 1 if you have the 'copy_file_range' function. */ -/* #undef HAVE_COPY_FILE_RANGE */ - -/* Define to 1 if you have the 'ctermid' function. */ -#define HAVE_CTERMID 1 - -/* Define if you have the 'ctermid_r' function. */ -#define HAVE_CTERMID_R 1 - -/* Define if you have the 'filter' function. */ -/* #undef HAVE_CURSES_FILTER */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CURSES_H */ - -/* Define if you have the 'has_key' function. */ -/* #undef HAVE_CURSES_HAS_KEY */ - -/* Define if you have the 'immedok' function. */ -/* #undef HAVE_CURSES_IMMEDOK */ - -/* Define if you have the 'is_pad' function. */ -/* #undef HAVE_CURSES_IS_PAD */ - -/* Define if you have the 'is_term_resized' function. */ -/* #undef HAVE_CURSES_IS_TERM_RESIZED */ - -/* Define if you have the 'resizeterm' function. */ -/* #undef HAVE_CURSES_RESIZETERM */ - -/* Define if you have the 'resize_term' function. */ -/* #undef HAVE_CURSES_RESIZE_TERM */ - -/* Define if you have the 'syncok' function. */ -/* #undef HAVE_CURSES_SYNCOK */ - -/* Define if you have the 'typeahead' function. */ -/* #undef HAVE_CURSES_TYPEAHEAD */ - -/* Define if you have the 'use_env' function. */ -/* #undef HAVE_CURSES_USE_ENV */ - -/* Define if you have the 'wchgat' function. */ -/* #undef HAVE_CURSES_WCHGAT */ - -/* Define to 1 if you have the header file. */ -#define HAVE_DB_H 1 - -/* Define to 1 if you have the declaration of 'RTLD_DEEPBIND', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_DEEPBIND 0 - -/* Define to 1 if you have the declaration of 'RTLD_GLOBAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_GLOBAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_LAZY', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LAZY 1 - -/* Define to 1 if you have the declaration of 'RTLD_LOCAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LOCAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_MEMBER', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_MEMBER 0 - -/* Define to 1 if you have the declaration of 'RTLD_NODELETE', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NODELETE 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOLOAD', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOLOAD 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOW', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOW 1 - -/* Define to 1 if you have the declaration of 'tzname', and to 0 if you don't. - */ -/* #undef HAVE_DECL_TZNAME */ - -/* Define to 1 if you have the declaration of 'UT_NAMESIZE', and to 0 if you - don't. */ -#define HAVE_DECL_UT_NAMESIZE 0 - -/* Define to 1 if you have the device macros. */ -#define HAVE_DEVICE_MACROS 1 - -/* Define to 1 if you have the /dev/ptc device file. */ -/* #undef HAVE_DEV_PTC */ - -/* Define to 1 if you have the /dev/ptmx device file. */ -/* #undef HAVE_DEV_PTMX */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_DIRECT_H */ - -/* Define to 1 if the dirent structure has a d_type field */ -#define HAVE_DIRENT_D_TYPE 1 - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -#define HAVE_DIRENT_H 1 - -/* Define if you have the 'dirfd' function or macro. */ -#define HAVE_DIRFD 1 - -/* Define to 1 if you have the 'dladdr' function. */ -#define HAVE_DLADDR 1 - -/* Define to 1 if you have the 'dladdr1' function. */ -/* #undef HAVE_DLADDR1 */ - -/* Define to 1 if you have the header file. */ -#define HAVE_DLFCN_H 1 - -/* Define to 1 if you have the 'dlopen' function. */ -#define HAVE_DLOPEN 1 - -/* Define to 1 if you have the 'dup' function. */ -#define HAVE_DUP 1 - -/* Define to 1 if you have the 'dup2' function. */ -#define HAVE_DUP2 1 - -/* Define to 1 if you have the 'dup3' function. */ -/* #undef HAVE_DUP3 */ - -/* Define if you have the '_dyld_shared_cache_contains_path' function. */ -#define HAVE_DYLD_SHARED_CACHE_CONTAINS_PATH 1 - -/* Defined when any dynamic module loading is enabled. */ -#define HAVE_DYNAMIC_LOADING 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_EDITLINE_READLINE_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_ENDIAN_H 1 - -/* Define if you have the 'epoll_create' function. */ -/* #undef HAVE_EPOLL */ - -/* Define if you have the 'epoll_create1' function. */ -/* #undef HAVE_EPOLL_CREATE1 */ - -/* Define to 1 if you have the 'erf' function. */ -#define HAVE_ERF 1 - -/* Define to 1 if you have the 'erfc' function. */ -#define HAVE_ERFC 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ERRNO_H 1 - -/* Define if you have the 'eventfd' function. */ -/* #undef HAVE_EVENTFD */ - -/* Define to 1 if you have the header file. */ -#define HAVE_EXECINFO_H 1 - -/* Define to 1 if you have the 'execv' function. */ -#define HAVE_EXECV 1 - -/* Define to 1 if you have the 'explicit_bzero' function. */ -/* #undef HAVE_EXPLICIT_BZERO */ - -/* Define to 1 if you have the 'explicit_memset' function. */ -/* #undef HAVE_EXPLICIT_MEMSET */ - -/* Define to 1 if you have the 'expm1' function. */ -#define HAVE_EXPM1 1 - -/* Define to 1 if you have the 'faccessat' function. */ -#define HAVE_FACCESSAT 1 - -/* Define if you have the 'fchdir' function. */ -#define HAVE_FCHDIR 1 - -/* Define to 1 if you have the 'fchmod' function. */ -#define HAVE_FCHMOD 1 - -/* Define to 1 if you have the 'fchmodat' function. */ -#define HAVE_FCHMODAT 1 - -/* Define to 1 if you have the 'fchown' function. */ -#define HAVE_FCHOWN 1 - -/* Define to 1 if you have the 'fchownat' function. */ -#define HAVE_FCHOWNAT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_FCNTL_H 1 - -/* Define if you have the 'fdatasync' function. */ -/* #undef HAVE_FDATASYNC */ - -/* Define to 1 if you have the 'fdopendir' function. */ -#define HAVE_FDOPENDIR 1 - -/* Define to 1 if you have the 'fdwalk' function. */ -/* #undef HAVE_FDWALK */ - -/* Define to 1 if you have the 'fexecve' function. */ -/* #undef HAVE_FEXECVE */ - -/* Define if you have the 'ffi_closure_alloc' function. */ -#define HAVE_FFI_CLOSURE_ALLOC 1 - -/* Define if you have the 'ffi_prep_cif_var' function. */ -#define HAVE_FFI_PREP_CIF_VAR 1 - -/* Define if you have the 'ffi_prep_closure_loc' function. */ -#define HAVE_FFI_PREP_CLOSURE_LOC 1 - -/* Define to 1 if you have the 'flock' function. */ -#define HAVE_FLOCK 1 - -/* Define to 1 if you have the 'fork' function. */ -#define HAVE_FORK 1 - -/* Define to 1 if you have the 'fork1' function. */ -/* #undef HAVE_FORK1 */ - -/* Define to 1 if you have the 'forkpty' function. */ -#define HAVE_FORKPTY 1 - -/* Define to 1 if you have the 'fpathconf' function. */ -#define HAVE_FPATHCONF 1 - -/* Define to 1 if you have the 'fseek64' function. */ -/* #undef HAVE_FSEEK64 */ - -/* Define to 1 if you have the 'fseeko' function. */ -#define HAVE_FSEEKO 1 - -/* Define to 1 if you have the 'fstatat' function. */ -#define HAVE_FSTATAT 1 - -/* Define to 1 if you have the 'fstatvfs' function. */ -#define HAVE_FSTATVFS 1 - -/* Define if you have the 'fsync' function. */ -#define HAVE_FSYNC 1 - -/* Define to 1 if you have the 'ftell64' function. */ -/* #undef HAVE_FTELL64 */ - -/* Define to 1 if you have the 'ftello' function. */ -#define HAVE_FTELLO 1 - -/* Define to 1 if you have the 'ftime' function. */ -#define HAVE_FTIME 1 - -/* Define to 1 if you have the 'ftruncate' function. */ -#define HAVE_FTRUNCATE 1 - -/* Define to 1 if you have the 'futimens' function. */ -#define HAVE_FUTIMENS 1 - -/* Define to 1 if you have the 'futimes' function. */ -#define HAVE_FUTIMES 1 - -/* Define to 1 if you have the 'futimesat' function. */ -/* #undef HAVE_FUTIMESAT */ - -/* Define to 1 if you have the 'gai_strerror' function. */ -#define HAVE_GAI_STRERROR 1 - -/* Define if we can use gcc inline assembler to get and set mc68881 fpcr */ -/* #undef HAVE_GCC_ASM_FOR_MC68881 */ - -/* Define if we can use x64 gcc inline assembler */ -/* #undef HAVE_GCC_ASM_FOR_X64 */ - -/* Define if we can use gcc inline assembler to get and set x87 control word - */ -/* #undef HAVE_GCC_ASM_FOR_X87 */ - -/* Define if your compiler provides __uint128_t */ -#define HAVE_GCC_UINT128_T 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_DASH_NDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_NDBM_H */ - -/* Define if you have the getaddrinfo function. */ -#define HAVE_GETADDRINFO 1 - -/* Define this if you have flockfile(), getc_unlocked(), and funlockfile() */ -#define HAVE_GETC_UNLOCKED 1 - -/* Define to 1 if you have the 'getegid' function. */ -#define HAVE_GETEGID 1 - -/* Define to 1 if you have the 'getentropy' function. */ -/* #undef HAVE_GETENTROPY */ - -/* Define to 1 if you have the 'geteuid' function. */ -#define HAVE_GETEUID 1 - -/* Define to 1 if you have the 'getgid' function. */ -#define HAVE_GETGID 1 - -/* Define to 1 if you have the 'getgrent' function. */ -#define HAVE_GETGRENT 1 - -/* Define to 1 if you have the 'getgrgid' function. */ -#define HAVE_GETGRGID 1 - -/* Define to 1 if you have the 'getgrgid_r' function. */ -#define HAVE_GETGRGID_R 1 - -/* Define to 1 if you have the 'getgrnam_r' function. */ -#define HAVE_GETGRNAM_R 1 - -/* Define to 1 if you have the 'getgrouplist' function. */ -#define HAVE_GETGROUPLIST 1 - -/* Define to 1 if you have the 'getgroups' function. */ -/* #undef HAVE_GETGROUPS */ - -/* Define if you have the 'gethostbyaddr' function. */ -#define HAVE_GETHOSTBYADDR 1 - -/* Define to 1 if you have the 'gethostbyname' function. */ -#define HAVE_GETHOSTBYNAME 1 - -/* Define this if you have some version of gethostbyname_r() */ -/* #undef HAVE_GETHOSTBYNAME_R */ - -/* Define this if you have the 3-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_3_ARG */ - -/* Define this if you have the 5-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_5_ARG */ - -/* Define this if you have the 6-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_6_ARG */ - -/* Define to 1 if you have the 'gethostname' function. */ -#define HAVE_GETHOSTNAME 1 - -/* Define to 1 if you have the 'getitimer' function. */ -#define HAVE_GETITIMER 1 - -/* Define to 1 if you have the 'getloadavg' function. */ -#define HAVE_GETLOADAVG 1 - -/* Define to 1 if you have the 'getlogin' function. */ -#define HAVE_GETLOGIN 1 - -/* Define to 1 if you have the 'getlogin_r' function. */ -#define HAVE_GETLOGIN_R 1 - -/* Define to 1 if you have the 'getnameinfo' function. */ -#define HAVE_GETNAMEINFO 1 - -/* Define if you have the 'getpagesize' function. */ -#define HAVE_GETPAGESIZE 1 - -/* Define if you have the 'getpeername' function. */ -#define HAVE_GETPEERNAME 1 - -/* Define to 1 if you have the 'getpgid' function. */ -#define HAVE_GETPGID 1 - -/* Define to 1 if you have the 'getpgrp' function. */ -#define HAVE_GETPGRP 1 - -/* Define to 1 if you have the 'getpid' function. */ -#define HAVE_GETPID 1 - -/* Define to 1 if you have the 'getppid' function. */ -#define HAVE_GETPPID 1 - -/* Define to 1 if you have the 'getpriority' function. */ -#define HAVE_GETPRIORITY 1 - -/* Define if you have the 'getprotobyname' function. */ -#define HAVE_GETPROTOBYNAME 1 - -/* Define to 1 if you have the 'getpwent' function. */ -#define HAVE_GETPWENT 1 - -/* Define to 1 if you have the 'getpwnam_r' function. */ -#define HAVE_GETPWNAM_R 1 - -/* Define to 1 if you have the 'getpwuid' function. */ -#define HAVE_GETPWUID 1 - -/* Define to 1 if you have the 'getpwuid_r' function. */ -#define HAVE_GETPWUID_R 1 - -/* Define to 1 if the getrandom() function is available */ -/* #undef HAVE_GETRANDOM */ - -/* Define to 1 if the Linux getrandom() syscall is available */ -/* #undef HAVE_GETRANDOM_SYSCALL */ - -/* Define to 1 if you have the 'getresgid' function. */ -/* #undef HAVE_GETRESGID */ - -/* Define to 1 if you have the 'getresuid' function. */ -/* #undef HAVE_GETRESUID */ - -/* Define to 1 if you have the 'getrusage' function. */ -#define HAVE_GETRUSAGE 1 - -/* Define if you have the 'getservbyname' function. */ -#define HAVE_GETSERVBYNAME 1 - -/* Define if you have the 'getservbyport' function. */ -#define HAVE_GETSERVBYPORT 1 - -/* Define to 1 if you have the 'getsid' function. */ -#define HAVE_GETSID 1 - -/* Define if you have the 'getsockname' function. */ -#define HAVE_GETSOCKNAME 1 - -/* Define to 1 if you have the 'getspent' function. */ -/* #undef HAVE_GETSPENT */ - -/* Define to 1 if you have the 'getspnam' function. */ -/* #undef HAVE_GETSPNAM */ - -/* Define to 1 if you have the 'getuid' function. */ -#define HAVE_GETUID 1 - -/* Define to 1 if you have the 'getwd' function. */ -#define HAVE_GETWD 1 - -/* Define if glibc has incorrect _FORTIFY_SOURCE wrappers for memmove and - bcopy. */ -/* #undef HAVE_GLIBC_MEMMOVE_BUG */ - -/* Define to 1 if you have the 'grantpt' function. */ -#define HAVE_GRANTPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_GRP_H 1 - -/* Define if you have the 'hstrerror' function. */ -#define HAVE_HSTRERROR 1 - -/* Define this if you have le64toh() */ -/* #undef HAVE_HTOLE64 */ - -/* Define to 1 if you have the 'if_nameindex' function. */ -#define HAVE_IF_NAMEINDEX 1 - -/* Define if you have the 'inet_aton' function. */ -#define HAVE_INET_ATON 1 - -/* Define if you have the 'inet_ntoa' function. */ -#define HAVE_INET_NTOA 1 - -/* Define if you have the 'inet_pton' function. */ -#define HAVE_INET_PTON 1 - -/* Define to 1 if you have the 'initgroups' function. */ -#define HAVE_INITGROUPS 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_INTTYPES_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_IO_H */ - -/* Define if gcc has the ipa-pure-const bug. */ -/* #undef HAVE_IPA_PURE_CONST_BUG */ - -/* Define to 1 if you have the 'kill' function. */ -#define HAVE_KILL 1 - -/* Define to 1 if you have the 'killpg' function. */ -#define HAVE_KILLPG 1 - -/* Define if you have the 'kqueue' function. */ -#define HAVE_KQUEUE 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LANGINFO_H 1 - -/* Defined to enable large file support when an off_t is bigger than a long - and long long is at least as big as an off_t. You may need to add some - flags for configuration and compilation to enable this mode. (For Solaris - and Linux, the necessary defines are already defined.) */ -/* #undef HAVE_LARGEFILE_SUPPORT */ - -/* Define to 1 if you have the 'lchflags' function. */ -#define HAVE_LCHFLAGS 1 - -/* Define to 1 if you have the 'lchmod' function. */ -#define HAVE_LCHMOD 1 - -/* Define to 1 if you have the 'lchown' function. */ -#define HAVE_LCHOWN 1 - -/* Define to 1 if you have the `db' library (-ldb). */ -/* #undef HAVE_LIBDB */ - -/* Define to 1 if you have the 'dl' library (-ldl). */ -#define HAVE_LIBDL 1 - -/* Define to 1 if you have the 'dld' library (-ldld). */ -/* #undef HAVE_LIBDLD */ - -/* Define to 1 if you have the 'ieee' library (-lieee). */ -/* #undef HAVE_LIBIEEE */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBINTL_H */ - -/* Define to 1 if you have the 'sendfile' library (-lsendfile). */ -/* #undef HAVE_LIBSENDFILE */ - -/* Define to 1 if you have the 'sqlite3' library (-lsqlite3). */ -#define HAVE_LIBSQLITE3 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBUTIL_H */ - -/* Define if you have the 'link' function. */ -#define HAVE_LINK 1 - -/* Define to 1 if you have the 'linkat' function. */ -#define HAVE_LINKAT 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_AUXVEC_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_BCM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_J1939_H */ - -/* Define if compiling using Linux 3.6 or later. */ -/* #undef HAVE_LINUX_CAN_RAW_FD_FRAMES */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_RAW_H */ - -/* Define if compiling using Linux 4.1 or later. */ -/* #undef HAVE_LINUX_CAN_RAW_JOIN_FILTERS */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_FS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_LIMITS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_MEMFD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_NETFILTER_IPV4_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_NETLINK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_QRTR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_RANDOM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_SCHED_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_SOUNDCARD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_TIPC_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_VM_SOCKETS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_WAIT_H */ - -/* Define if you have the 'listen' function. */ -#define HAVE_LISTEN 1 - -/* Define to 1 if you have the 'lockf' function. */ -#define HAVE_LOCKF 1 - -/* Define to 1 if you have the 'log1p' function. */ -#define HAVE_LOG1P 1 - -/* Define to 1 if you have the 'log2' function. */ -#define HAVE_LOG2 1 - -/* Define to 1 if you have the `login_tty' function. */ -#define HAVE_LOGIN_TTY 1 - -/* Define to 1 if the system has the type 'long double'. */ -#define HAVE_LONG_DOUBLE 1 - -/* Define to 1 if you have the 'lstat' function. */ -#define HAVE_LSTAT 1 - -/* Define to 1 if you have the 'lutimes' function. */ -#define HAVE_LUTIMES 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LZMA_H */ - -/* Define to 1 if you have the 'madvise' function. */ -#define HAVE_MADVISE 1 - -/* Define this if you have the makedev macro. */ -#define HAVE_MAKEDEV 1 - -/* Define if you have the 'MAXLOGNAME' constant. */ -#define HAVE_MAXLOGNAME 1 - -/* Define to 1 if you have the 'mbrtowc' function. */ -#define HAVE_MBRTOWC 1 - -/* Define if you have the 'memfd_create' function. */ -/* #undef HAVE_MEMFD_CREATE */ - -/* Define to 1 if you have the 'memrchr' function. */ -/* #undef HAVE_MEMRCHR */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_MINIX_CONFIG_H */ - -/* Define to 1 if you have the 'mkdirat' function. */ -#define HAVE_MKDIRAT 1 - -/* Define to 1 if you have the 'mkfifo' function. */ -#define HAVE_MKFIFO 1 - -/* Define to 1 if you have the 'mkfifoat' function. */ -#define HAVE_MKFIFOAT 1 - -/* Define to 1 if you have the 'mknod' function. */ -#define HAVE_MKNOD 1 - -/* Define to 1 if you have the 'mknodat' function. */ -#define HAVE_MKNODAT 1 - -/* Define to 1 if you have the 'mktime' function. */ -#define HAVE_MKTIME 1 - -/* Define to 1 if you have the 'mmap' function. */ -#define HAVE_MMAP 1 - -/* Define to 1 if you have the 'mremap' function. */ -/* #undef HAVE_MREMAP */ - -/* Define to 1 if you have the 'nanosleep' function. */ -#define HAVE_NANOSLEEP 1 - -/* Define if you have the 'ncurses' library */ -/* #undef HAVE_NCURSES */ - -/* Define if you have the 'ncursesw' library */ -/* #undef HAVE_NCURSESW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_PANEL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_PANEL_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NDBM_H 1 - -/* Define to 1 if you have the header file, and it defines 'DIR'. */ -/* #undef HAVE_NDIR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETCAN_CAN_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NETDB_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NETINET_IN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETLINK_NETLINK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETPACKET_PACKET_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_ETHERNET_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_IF_H 1 - -/* Define to 1 if you have the 'nice' function. */ -#define HAVE_NICE 1 - -/* Define if the internal form of wchar_t in non-Unicode locales is not - Unicode. */ -/* #undef HAVE_NON_UNICODE_WCHAR_T_REPRESENTATION */ - -/* Define to 1 if you have the 'openat' function. */ -#define HAVE_OPENAT 1 - -/* Define to 1 if you have the 'opendir' function. */ -#define HAVE_OPENDIR 1 - -/* Define to 1 if you have the 'openpty' function. */ -#define HAVE_OPENPTY 1 - -/* Define if you have the 'panel' library */ -/* #undef HAVE_PANEL */ - -/* Define if you have the 'panelw' library */ -/* #undef HAVE_PANELW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PANEL_H */ - -/* Define to 1 if you have the 'pathconf' function. */ -#define HAVE_PATHCONF 1 - -/* Define to 1 if you have the 'pause' function. */ -#define HAVE_PAUSE 1 - -/* Define to 1 if you have the 'pipe' function. */ -#define HAVE_PIPE 1 - -/* Define to 1 if you have the 'pipe2' function. */ -/* #undef HAVE_PIPE2 */ - -/* Define to 1 if you have the 'plock' function. */ -/* #undef HAVE_PLOCK */ - -/* Define to 1 if you have the 'poll' function. */ -#define HAVE_POLL 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_POLL_H 1 - -/* Define to 1 if you have the 'posix_fadvise' function. */ -/* #undef HAVE_POSIX_FADVISE */ - -/* Define to 1 if you have the 'posix_fallocate' function. */ -/* #undef HAVE_POSIX_FALLOCATE */ - -/* Define to 1 if you have the 'posix_openpt' function. */ -#define HAVE_POSIX_OPENPT 1 - -/* Define to 1 if you have the 'posix_spawn' function. */ -#define HAVE_POSIX_SPAWN 1 - -/* Define to 1 if you have the 'posix_spawnp' function. */ -#define HAVE_POSIX_SPAWNP 1 - -/* Define to 1 if you have the 'posix_spawn_file_actions_addclosefrom_np' - function. */ -/* #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP */ - -/* Define to 1 if you have the 'pread' function. */ -#define HAVE_PREAD 1 - -/* Define to 1 if you have the 'preadv' function. */ -#define HAVE_PREADV 1 - -/* Define to 1 if you have the 'preadv2' function. */ -/* #undef HAVE_PREADV2 */ - -/* Define if you have the 'prlimit' function. */ -/* #undef HAVE_PRLIMIT */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PROCESS_H */ - -/* Define to 1 if you have the 'process_vm_readv' function. */ -/* #undef HAVE_PROCESS_VM_READV */ - -/* Define if your compiler supports function prototype */ -#define HAVE_PROTOTYPES 1 - -/* Define to 1 if you have the 'pthread_condattr_setclock' function. */ -/* #undef HAVE_PTHREAD_CONDATTR_SETCLOCK */ - -/* Define to 1 if you have the 'pthread_cond_timedwait_relative_np' function. - */ -#define HAVE_PTHREAD_COND_TIMEDWAIT_RELATIVE_NP 1 - -/* Defined for Solaris 2.6 bug in pthread header. */ -/* #undef HAVE_PTHREAD_DESTRUCTOR */ - -/* Define to 1 if you have the 'pthread_getattr_np' function. */ -/* #undef HAVE_PTHREAD_GETATTR_NP */ - -/* Define to 1 if you have the 'pthread_getcpuclockid' function. */ -/* #undef HAVE_PTHREAD_GETCPUCLOCKID */ - -/* Define to 1 if you have the 'pthread_getname_np' function. */ -#define HAVE_PTHREAD_GETNAME_NP 1 - -/* Define to 1 if you have the 'pthread_get_name_np' function. */ -/* #undef HAVE_PTHREAD_GET_NAME_NP */ - -/* Define to 1 if you have the header file. */ -#define HAVE_PTHREAD_H 1 - -/* Define to 1 if you have the 'pthread_init' function. */ -/* #undef HAVE_PTHREAD_INIT */ - -/* Define to 1 if you have the 'pthread_kill' function. */ -#define HAVE_PTHREAD_KILL 1 - -/* Define to 1 if you have the 'pthread_setname_np' function. */ -#define HAVE_PTHREAD_SETNAME_NP 1 - -/* Define to 1 if you have the 'pthread_set_name_np' function. */ -/* #undef HAVE_PTHREAD_SET_NAME_NP */ - -/* Define to 1 if you have the 'pthread_sigmask' function. */ -#define HAVE_PTHREAD_SIGMASK 1 - -/* Define if platform requires stubbed pthreads support */ -/* #undef HAVE_PTHREAD_STUBS */ - -/* Define to 1 if you have the 'ptsname' function. */ -#define HAVE_PTSNAME 1 - -/* Define to 1 if you have the 'ptsname_r' function. */ -#define HAVE_PTSNAME_R 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PTY_H */ - -/* Define to 1 if you have the 'pwrite' function. */ -#define HAVE_PWRITE 1 - -/* Define to 1 if you have the 'pwritev' function. */ -#define HAVE_PWRITEV 1 - -/* Define to 1 if you have the 'pwritev2' function. */ -/* #undef HAVE_PWRITEV2 */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_READLINE_READLINE_H */ - -/* Define to 1 if you have the 'readlink' function. */ -#define HAVE_READLINK 1 - -/* Define to 1 if you have the 'readlinkat' function. */ -#define HAVE_READLINKAT 1 - -/* Define to 1 if you have the 'readv' function. */ -#define HAVE_READV 1 - -/* Define to 1 if you have the 'realpath' function. */ -#define HAVE_REALPATH 1 - -/* Define if you have the 'recvfrom' function. */ -#define HAVE_RECVFROM 1 - -/* Define to 1 if you have the 'renameat' function. */ -#define HAVE_RENAMEAT 1 - -/* Define if readline supports append_history */ -/* #undef HAVE_RL_APPEND_HISTORY */ - -/* Define if you can turn off readline's signal handling. */ -/* #undef HAVE_RL_CATCH_SIGNAL */ - -/* Define to 1 if the system has the type 'rl_compdisp_func_t'. */ -/* #undef HAVE_RL_COMPDISP_FUNC_T */ - -/* Define if you have readline 2.2 */ -/* #undef HAVE_RL_COMPLETION_APPEND_CHARACTER */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_COMPLETION_DISPLAY_MATCHES_HOOK */ - -/* Define if you have readline 4.2 */ -/* #undef HAVE_RL_COMPLETION_MATCHES */ - -/* Define if you have rl_completion_suppress_append */ -/* #undef HAVE_RL_COMPLETION_SUPPRESS_APPEND */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_PRE_INPUT_HOOK */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_RESIZE_TERMINAL */ - -/* Define to 1 if you have the 'rtpSpawn' function. */ -/* #undef HAVE_RTPSPAWN */ - -/* Define to 1 if you have the 'sched_get_priority_max' function. */ -#define HAVE_SCHED_GET_PRIORITY_MAX 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SCHED_H 1 - -/* Define to 1 if you have the 'sched_rr_get_interval' function. */ -/* #undef HAVE_SCHED_RR_GET_INTERVAL */ - -/* Define to 1 if you have the 'sched_setaffinity' function. */ -/* #undef HAVE_SCHED_SETAFFINITY */ - -/* Define to 1 if you have the 'sched_setparam' function. */ -/* #undef HAVE_SCHED_SETPARAM */ - -/* Define to 1 if you have the 'sched_setscheduler' function. */ -/* #undef HAVE_SCHED_SETSCHEDULER */ - -/* Define to 1 if you have the 'sem_clockwait' function. */ -/* #undef HAVE_SEM_CLOCKWAIT */ - -/* Define to 1 if you have the 'sem_getvalue' function. */ -#define HAVE_SEM_GETVALUE 1 - -/* Define to 1 if you have the 'sem_open' function. */ -#define HAVE_SEM_OPEN 1 - -/* Define to 1 if you have the 'sem_timedwait' function. */ -/* #undef HAVE_SEM_TIMEDWAIT */ - -/* Define to 1 if you have the 'sem_unlink' function. */ -#define HAVE_SEM_UNLINK 1 - -/* Define to 1 if you have the 'sendfile' function. */ -#define HAVE_SENDFILE 1 - -/* Define if you have the 'sendto' function. */ -#define HAVE_SENDTO 1 - -/* Define to 1 if you have the 'setegid' function. */ -#define HAVE_SETEGID 1 - -/* Define to 1 if you have the 'seteuid' function. */ -#define HAVE_SETEUID 1 - -/* Define to 1 if you have the 'setgid' function. */ -#define HAVE_SETGID 1 - -/* Define if you have the 'setgroups' function. */ -#define HAVE_SETGROUPS 1 - -/* Define to 1 if you have the 'sethostname' function. */ -#define HAVE_SETHOSTNAME 1 - -/* Define to 1 if you have the 'setitimer' function. */ -#define HAVE_SETITIMER 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SETJMP_H 1 - -/* Define to 1 if you have the 'setlocale' function. */ -#define HAVE_SETLOCALE 1 - -/* Define to 1 if you have the 'setns' function. */ -/* #undef HAVE_SETNS */ - -/* Define to 1 if you have the 'setpgid' function. */ -#define HAVE_SETPGID 1 - -/* Define to 1 if you have the 'setpgrp' function. */ -#define HAVE_SETPGRP 1 - -/* Define to 1 if you have the 'setpriority' function. */ -#define HAVE_SETPRIORITY 1 - -/* Define to 1 if you have the 'setregid' function. */ -#define HAVE_SETREGID 1 - -/* Define to 1 if you have the 'setresgid' function. */ -/* #undef HAVE_SETRESGID */ - -/* Define to 1 if you have the 'setresuid' function. */ -/* #undef HAVE_SETRESUID */ - -/* Define to 1 if you have the 'setreuid' function. */ -#define HAVE_SETREUID 1 - -/* Define to 1 if you have the 'setsid' function. */ -#define HAVE_SETSID 1 - -/* Define if you have the 'setsockopt' function. */ -#define HAVE_SETSOCKOPT 1 - -/* Define to 1 if you have the 'setuid' function. */ -#define HAVE_SETUID 1 - -/* Define to 1 if you have the 'setvbuf' function. */ -#define HAVE_SETVBUF 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SHADOW_H */ - -/* Define to 1 if you have the 'shm_open' function. */ -#define HAVE_SHM_OPEN 1 - -/* Define to 1 if you have the 'shm_unlink' function. */ -#define HAVE_SHM_UNLINK 1 - -/* Define to 1 if you have the 'shutdown' function. */ -#define HAVE_SHUTDOWN 1 - -/* Define to 1 if you have the 'sigaction' function. */ -#define HAVE_SIGACTION 1 - -/* Define to 1 if you have the 'sigaltstack' function. */ -#define HAVE_SIGALTSTACK 1 - -/* Define to 1 if you have the 'sigfillset' function. */ -#define HAVE_SIGFILLSET 1 - -/* Define to 1 if 'si_band' is a member of 'siginfo_t'. */ -#define HAVE_SIGINFO_T_SI_BAND 1 - -/* Define to 1 if you have the 'siginterrupt' function. */ -#define HAVE_SIGINTERRUPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SIGNAL_H 1 - -/* Define to 1 if you have the 'sigpending' function. */ -#define HAVE_SIGPENDING 1 - -/* Define to 1 if you have the 'sigrelse' function. */ -#define HAVE_SIGRELSE 1 - -/* Define to 1 if you have the 'sigtimedwait' function. */ -/* #undef HAVE_SIGTIMEDWAIT */ - -/* Define to 1 if you have the 'sigwait' function. */ -#define HAVE_SIGWAIT 1 - -/* Define to 1 if you have the 'sigwaitinfo' function. */ -/* #undef HAVE_SIGWAITINFO */ - -/* Define to 1 if you have the 'snprintf' function. */ -#define HAVE_SNPRINTF 1 - -/* struct sockaddr_alg (linux/if_alg.h) */ -/* #undef HAVE_SOCKADDR_ALG */ - -/* Define if sockaddr has sa_len member */ -#define HAVE_SOCKADDR_SA_LEN 1 - -/* struct sockaddr_storage (sys/socket.h) */ -#define HAVE_SOCKADDR_STORAGE 1 - -/* Define if you have the 'socket' function. */ -#define HAVE_SOCKET 1 - -/* Define if you have the 'socketpair' function. */ -#define HAVE_SOCKETPAIR 1 - -/* Define to 1 if the system has the type 'socklen_t'. */ -#define HAVE_SOCKLEN_T 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SPAWN_H 1 - -/* Define to 1 if you have the 'splice' function. */ -/* #undef HAVE_SPLICE */ - -/* Define to 1 if the system has the type 'ssize_t'. */ -#define HAVE_SSIZE_T 1 - -/* Define to 1 if you have the 'statvfs' function. */ -#define HAVE_STATVFS 1 - -/* Define if you have struct stat.st_mtim.tv_nsec */ -/* #undef HAVE_STAT_TV_NSEC */ - -/* Define if you have struct stat.st_mtimensec */ -#define HAVE_STAT_TV_NSEC2 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDINT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDLIB_H 1 - -/* Has stdatomic.h with atomic_int and atomic_uintptr_t */ -#define HAVE_STD_ATOMIC 1 - -/* Define to 1 if you have the 'strftime' function. */ -#define HAVE_STRFTIME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRINGS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRING_H 1 - -/* Define to 1 if you have the 'strlcpy' function. */ -#define HAVE_STRLCPY 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_STROPTS_H */ - -/* Define to 1 if you have the 'strsignal' function. */ -#define HAVE_STRSIGNAL 1 - -/* Define to 1 if 'pw_gecos' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_GECOS 1 - -/* Define to 1 if 'pw_passwd' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_PASSWD 1 - -/* Define to 1 if 'st_birthtime' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BIRTHTIME 1 - -/* Define to 1 if 'st_blksize' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLKSIZE 1 - -/* Define to 1 if 'st_blocks' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLOCKS 1 - -/* Define to 1 if 'st_flags' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_FLAGS 1 - -/* Define to 1 if 'st_gen' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_GEN 1 - -/* Define to 1 if 'st_rdev' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_RDEV 1 - -/* Define to 1 if 'tm_zone' is a member of 'struct tm'. */ -#define HAVE_STRUCT_TM_TM_ZONE 1 - -/* Define if you have the 'symlink' function. */ -#define HAVE_SYMLINK 1 - -/* Define to 1 if you have the 'symlinkat' function. */ -#define HAVE_SYMLINKAT 1 - -/* Define to 1 if you have the 'sync' function. */ -#define HAVE_SYNC 1 - -/* Define to 1 if you have the 'sysconf' function. */ -#define HAVE_SYSCONF 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSEXITS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSLOG_H 1 - -/* Define to 1 if you have the 'system' function. */ -/* #undef HAVE_SYSTEM */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_AUDIOIO_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_AUXV_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_BSDTTY_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_DEVPOLL_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_DIR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_ENDIAN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_EPOLL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_EVENTFD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_EVENT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_FILE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_IOCTL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_KERN_CONTROL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_LOADAVG_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_LOCK_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MEMFD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MKDEV_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_MMAN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MODEM_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_NDIR_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_PARAM_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_PIDFD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_POLL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_RANDOM_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_RESOURCE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SELECT_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SENDFILE_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SOCKET_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SOUNDCARD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STATVFS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STAT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SYSCALL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYSMACROS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYS_DOMAIN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_TERMIO_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_TIMERFD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIMES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TYPES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UTSNAME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_WAIT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_XATTR_H 1 - -/* Define to 1 if you have the 'tcgetpgrp' function. */ -#define HAVE_TCGETPGRP 1 - -/* Define to 1 if you have the 'tcsetpgrp' function. */ -#define HAVE_TCSETPGRP 1 - -/* Define to 1 if you have the 'tempnam' function. */ -#define HAVE_TEMPNAM 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_TERMIOS_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_TERM_H */ - -/* Define to 1 if you have the 'timegm' function. */ -#define HAVE_TIMEGM 1 - -/* Define if you have the 'timerfd_create' function. */ -/* #undef HAVE_TIMERFD_CREATE */ - -/* Define to 1 if you have the 'times' function. */ -#define HAVE_TIMES 1 - -/* Define to 1 if you have the 'tmpfile' function. */ -#define HAVE_TMPFILE 1 - -/* Define to 1 if you have the 'tmpnam' function. */ -#define HAVE_TMPNAM 1 - -/* Define to 1 if you have the 'tmpnam_r' function. */ -/* #undef HAVE_TMPNAM_R */ - -/* Define to 1 if your 'struct tm' has 'tm_zone'. Deprecated, use - 'HAVE_STRUCT_TM_TM_ZONE' instead. */ -#define HAVE_TM_ZONE 1 - -/* Define to 1 if you have the 'truncate' function. */ -#define HAVE_TRUNCATE 1 - -/* Define to 1 if you have the 'ttyname_r' function. */ -#define HAVE_TTYNAME_R 1 - -/* Define to 1 if you don't have 'tm_zone' but do have the external array - 'tzname'. */ -/* #undef HAVE_TZNAME */ - -/* Define to 1 if you have the 'umask' function. */ -#define HAVE_UMASK 1 - -/* Define to 1 if you have the 'uname' function. */ -#define HAVE_UNAME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UNISTD_H 1 - -/* Define to 1 if you have the 'unlinkat' function. */ -#define HAVE_UNLINKAT 1 - -/* Define to 1 if you have the 'unlockpt' function. */ -#define HAVE_UNLOCKPT 1 - -/* Define to 1 if you have the 'unshare' function. */ -/* #undef HAVE_UNSHARE */ - -/* Define if you have a useable wchar_t type defined in wchar.h; useable means - wchar_t must be an unsigned type with at least 16 bits. (see - Include/unicodeobject.h). */ -/* #undef HAVE_USABLE_WCHAR_T */ - -/* Define to 1 if you have the header file. */ -#define HAVE_UTIL_H 1 - -/* Define to 1 if you have the 'utimensat' function. */ -#define HAVE_UTIMENSAT 1 - -/* Define to 1 if you have the 'utimes' function. */ -#define HAVE_UTIMES 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UTIME_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UTMP_H */ - -/* Define if you have the 'HAVE_UT_NAMESIZE' constant. */ -/* #undef HAVE_UT_NAMESIZE */ - -/* Define to 1 if you have the 'uuid_create' function. */ -/* #undef HAVE_UUID_CREATE */ - -/* Define to 1 if you have the 'uuid_enc_be' function. */ -/* #undef HAVE_UUID_ENC_BE */ - -/* Define if uuid_generate_time_safe() exists. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE */ - -/* Define if uuid_generate_time_safe() is able to deduce a MAC address. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE_STABLE_MAC */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UUID_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_UUID_UUID_H 1 - -/* Define to 1 if you have the 'vfork' function. */ -#define HAVE_VFORK 1 - -/* Define to 1 if you have the 'wait' function. */ -#define HAVE_WAIT 1 - -/* Define to 1 if you have the 'wait3' function. */ -#define HAVE_WAIT3 1 - -/* Define to 1 if you have the 'wait4' function. */ -#define HAVE_WAIT4 1 - -/* Define to 1 if you have the 'waitid' function. */ -#define HAVE_WAITID 1 - -/* Define to 1 if you have the 'waitpid' function. */ -#define HAVE_WAITPID 1 - -/* Define if the compiler provides a wchar.h header file. */ -#define HAVE_WCHAR_H 1 - -/* Define to 1 if you have the 'wcscoll' function. */ -#define HAVE_WCSCOLL 1 - -/* Define to 1 if you have the 'wcsftime' function. */ -#define HAVE_WCSFTIME 1 - -/* Define to 1 if you have the 'wcsxfrm' function. */ -#define HAVE_WCSXFRM 1 - -/* Define to 1 if you have the 'wmemcmp' function. */ -#define HAVE_WMEMCMP 1 - -/* Define if tzset() actually switches the local timezone in a meaningful way. - */ -/* #undef HAVE_WORKING_TZSET */ - -/* Define to 1 if you have the 'writev' function. */ -#define HAVE_WRITEV 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZDICT_H */ - -/* Define if the zlib library has inflateCopy */ -#define HAVE_ZLIB_COPY 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ZLIB_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZSTD_H */ - -/* Define to 1 if you have the '_getpty' function. */ -/* #undef HAVE__GETPTY */ - -/* Define to 1 if the system has the type '__uint128_t'. */ -#define HAVE___UINT128_T 1 - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in . - */ -/* #undef MAJOR_IN_MKDEV */ - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in - . */ -/* #undef MAJOR_IN_SYSMACROS */ - -/* Define if mvwdelch in curses.h is an expression. */ -/* #undef MVWDELCH_IS_EXPRESSION */ - -/* Define to the address where bug reports for this package should be sent. */ -/* #undef PACKAGE_BUGREPORT */ - -/* Define to the full name of this package. */ -/* #undef PACKAGE_NAME */ - -/* Define to the full name and version of this package. */ -/* #undef PACKAGE_STRING */ - -/* Define to the one symbol short name of this package. */ -/* #undef PACKAGE_TARNAME */ - -/* Define to the home page for this package. */ -/* #undef PACKAGE_URL */ - -/* Define to the version of this package. */ -/* #undef PACKAGE_VERSION */ - -/* Define if POSIX semaphores aren't enabled on your system */ -/* #undef POSIX_SEMAPHORES_NOT_ENABLED */ - -/* Define if pthread_key_t is compatible with int. */ -/* #undef PTHREAD_KEY_T_IS_COMPATIBLE_WITH_INT */ - -/* Defined if PTHREAD_SCOPE_SYSTEM supported. */ -/* #undef PTHREAD_SYSTEM_SCHED_SUPPORTED */ - -/* Define as the preferred size in bits of long digits */ -/* #undef PYLONG_BITS_IN_DIGIT */ - -/* enabled builtin hash modules */ -#define PY_BUILTIN_HASHLIB_HASHES "md5,sha1,sha2,sha3,blake2" - -/* Define if you want to coerce the C locale to a UTF-8 based locale */ -#define PY_COERCE_C_LOCALE 1 - -/* Define to 1 if you have the perf trampoline. */ -/* #undef PY_HAVE_PERF_TRAMPOLINE */ - -/* Define to 1 to build the sqlite module with loadable extensions support. */ -/* #undef PY_SQLITE_ENABLE_LOAD_EXTENSION */ - -/* Define if SQLite was compiled with the serialize API */ -#define PY_SQLITE_HAVE_SERIALIZE 1 - -/* Default cipher suites list for ssl module. 1: Python's preferred selection, - 2: leave OpenSSL defaults untouched, 0: custom string */ -#define PY_SSL_DEFAULT_CIPHERS 1 - -/* Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0 */ -/* #undef PY_SSL_DEFAULT_CIPHER_STRING */ - -/* PEP 11 Support tier (1, 2, 3 or 0 for unsupported) */ -#define PY_SUPPORT_TIER 3 - -/* Define if you want to build an interpreter with many run-time checks. */ -/* #undef Py_DEBUG */ - -/* Defined if Python is built as a shared library. */ -/* #undef Py_ENABLE_SHARED */ - -/* Define if you want to disable the GIL */ -/* #undef Py_GIL_DISABLED */ - -/* Define hash algorithm for str, bytes and memoryview. SipHash24: 1, FNV: 2, - SipHash13: 3, externally defined: 0 */ -/* #undef Py_HASH_ALGORITHM */ - -/* Define if you want to enable remote debugging support. */ -#define Py_REMOTE_DEBUG 1 - -/* Define if rl_startup_hook takes arguments */ -/* #undef Py_RL_STARTUP_HOOK_TAKES_ARGS */ - -/* Define if you want to enable internal statistics gathering. */ -/* #undef Py_STATS */ - -/* The version of SunOS/Solaris as reported by `uname -r' without the dot. */ -/* #undef Py_SUNOS_VERSION */ - -/* Define if you want to use tail-calling interpreters in CPython. */ -/* #undef Py_TAIL_CALL_INTERP */ - -/* Define if you want to enable tracing references for debugging purpose */ -/* #undef Py_TRACE_REFS */ - -/* assume C89 semantics that RETSIGTYPE is always void */ -#define RETSIGTYPE void - -/* Define if setpgrp() must be called as setpgrp(0, 0). */ -/* #undef SETPGRP_HAVE_ARG */ - -/* Define if i>>j for signed int i does not extend the sign bit when i < 0 */ -/* #undef SIGNED_RIGHT_SHIFT_ZERO_FILLS */ - -/* The size of 'double', as computed by sizeof. */ -#define SIZEOF_DOUBLE 8 - -/* The size of 'float', as computed by sizeof. */ -#define SIZEOF_FLOAT 4 - -/* The size of 'fpos_t', as computed by sizeof. */ -#define SIZEOF_FPOS_T 8 - -/* The size of 'int', as computed by sizeof. */ -#define SIZEOF_INT 4 - -/* The size of 'long', as computed by sizeof. */ -#define SIZEOF_LONG 8 - -/* The size of 'long double', as computed by sizeof. */ -#define SIZEOF_LONG_DOUBLE 8 - -/* The size of 'long long', as computed by sizeof. */ -#define SIZEOF_LONG_LONG 8 - -/* The size of 'off_t', as computed by sizeof. */ -#define SIZEOF_OFF_T 8 - -/* The size of 'pid_t', as computed by sizeof. */ -#define SIZEOF_PID_T 4 - -/* The size of 'pthread_key_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_KEY_T 8 - -/* The size of 'pthread_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_T 8 - -/* The size of 'short', as computed by sizeof. */ -#define SIZEOF_SHORT 2 - -/* The size of 'size_t', as computed by sizeof. */ -#define SIZEOF_SIZE_T 8 - -/* The size of 'time_t', as computed by sizeof. */ -#define SIZEOF_TIME_T 8 - -/* The size of 'uintptr_t', as computed by sizeof. */ -#define SIZEOF_UINTPTR_T 8 - -/* The size of 'void *', as computed by sizeof. */ -#define SIZEOF_VOID_P 8 - -/* The size of 'wchar_t', as computed by sizeof. */ -#define SIZEOF_WCHAR_T 4 - -/* The size of '_Bool', as computed by sizeof. */ -#define SIZEOF__BOOL 1 - -/* Define to 1 if you have the ANSI C header files. */ -#define STDC_HEADERS 1 - -/* Define if you can safely include both and - (which you can't on SCO ODT 3.0). */ -#define SYS_SELECT_WITH_SYS_TIME 1 - -/* Custom thread stack size depending on chosen sanitizer runtimes. */ -#define THREAD_STACK_SIZE 0x1000000 - -/* Library needed by timemodule.c: librt may be needed for clock_gettime() */ -/* #undef TIMEMODULE_LIB */ - -/* Define to 1 if your declares 'struct tm'. */ -/* #undef TM_IN_SYS_TIME */ - -/* Define if you want to use computed gotos in ceval.c. */ -/* #undef USE_COMPUTED_GOTOS */ - -/* Enable extensions on AIX, Interix, z/OS. */ -#ifndef _ALL_SOURCE -# define _ALL_SOURCE 1 -#endif -/* Enable general extensions on macOS. */ -#ifndef _DARWIN_C_SOURCE -# define _DARWIN_C_SOURCE 1 -#endif -/* Enable general extensions on Solaris. */ -#ifndef __EXTENSIONS__ -# define __EXTENSIONS__ 1 -#endif -/* Enable GNU extensions on systems that have them. */ -#ifndef _GNU_SOURCE -# define _GNU_SOURCE 1 -#endif -/* Enable X/Open compliant socket functions that do not require linking - with -lxnet on HP-UX 11.11. */ -#ifndef _HPUX_ALT_XOPEN_SOCKET_API -# define _HPUX_ALT_XOPEN_SOCKET_API 1 -#endif -/* Identify the host operating system as Minix. - This macro does not affect the system headers' behavior. - A future release of Autoconf may stop defining this macro. */ -#ifndef _MINIX -/* # undef _MINIX */ -#endif -/* Enable general extensions on NetBSD. - Enable NetBSD compatibility extensions on Minix. */ -#ifndef _NETBSD_SOURCE -# define _NETBSD_SOURCE 1 -#endif -/* Enable OpenBSD compatibility extensions on NetBSD. - Oddly enough, this does nothing on OpenBSD. */ -#ifndef _OPENBSD_SOURCE -# define _OPENBSD_SOURCE 1 -#endif -/* Define to 1 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_SOURCE -/* # undef _POSIX_SOURCE */ -#endif -/* Define to 2 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_1_SOURCE -/* # undef _POSIX_1_SOURCE */ -#endif -/* Enable POSIX-compatible threading on Solaris. */ -#ifndef _POSIX_PTHREAD_SEMANTICS -# define _POSIX_PTHREAD_SEMANTICS 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-5:2014. */ -#ifndef __STDC_WANT_IEC_60559_ATTRIBS_EXT__ -# define __STDC_WANT_IEC_60559_ATTRIBS_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-1:2014. */ -#ifndef __STDC_WANT_IEC_60559_BFP_EXT__ -# define __STDC_WANT_IEC_60559_BFP_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-2:2015. */ -#ifndef __STDC_WANT_IEC_60559_DFP_EXT__ -# define __STDC_WANT_IEC_60559_DFP_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex F. */ -#ifndef __STDC_WANT_IEC_60559_EXT__ -# define __STDC_WANT_IEC_60559_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-4:2015. */ -#ifndef __STDC_WANT_IEC_60559_FUNCS_EXT__ -# define __STDC_WANT_IEC_60559_FUNCS_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex H and ISO/IEC TS 18661-3:2015. */ -#ifndef __STDC_WANT_IEC_60559_TYPES_EXT__ -# define __STDC_WANT_IEC_60559_TYPES_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TR 24731-2:2010. */ -#ifndef __STDC_WANT_LIB_EXT2__ -# define __STDC_WANT_LIB_EXT2__ 1 -#endif -/* Enable extensions specified by ISO/IEC 24747:2009. */ -#ifndef __STDC_WANT_MATH_SPEC_FUNCS__ -# define __STDC_WANT_MATH_SPEC_FUNCS__ 1 -#endif -/* Enable extensions on HP NonStop. */ -#ifndef _TANDEM_SOURCE -# define _TANDEM_SOURCE 1 -#endif -/* Enable X/Open extensions. Define to 500 only if necessary - to make mbstate_t available. */ -#ifndef _XOPEN_SOURCE -/* # undef _XOPEN_SOURCE */ -#endif - - -/* Define if WINDOW in curses.h offers a field _flags. */ -/* #undef WINDOW_HAS_FLAGS */ - -/* Define if you want build the _decimal module using a coroutine-local rather - than a thread-local context */ -#define WITH_DECIMAL_CONTEXTVAR 1 - -/* Define if you want documentation strings in extension modules */ -#define WITH_DOC_STRINGS 1 - -/* Define if you want to compile in DTrace support */ -/* #undef WITH_DTRACE */ - -/* Define if you want to use the new-style (Openstep, Rhapsody, MacOS) dynamic - linker (dyld) instead of the old-style (NextStep) dynamic linker (rld). - Dyld is necessary to support frameworks. */ -/* #undef WITH_DYLD */ - -/* Define to build the readline module against libedit. */ -/* #undef WITH_EDITLINE */ - -/* Define to 1 if libintl is needed for locale functions. */ -/* #undef WITH_LIBINTL */ - -/* Define if you want to compile in mimalloc memory allocator. */ -#define WITH_MIMALLOC 1 - -/* Define if you want to produce an OpenStep/Rhapsody framework (shared - library plus accessory files). */ -#define WITH_NEXT_FRAMEWORK 1 - -/* Define if you want to compile in Python-specific mallocs */ -#define WITH_PYMALLOC 1 - -/* Define if you want pymalloc to be disabled when running under valgrind */ -/* #undef WITH_VALGRIND */ - -/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most - significant byte first (like Motorola and SPARC, unlike Intel). */ -#if defined AC_APPLE_UNIVERSAL_BUILD -# if defined __BIG_ENDIAN__ -# define WORDS_BIGENDIAN 1 -# endif -#else -# ifndef WORDS_BIGENDIAN -/* # undef WORDS_BIGENDIAN */ -# endif -#endif - -/* Define if arithmetic is subject to x87-style double rounding issue */ -/* #undef X87_DOUBLE_ROUNDING */ - -/* Define on OpenBSD to activate all library features */ -/* #undef _BSD_SOURCE */ - -/* Define on Darwin to activate all library features */ -#define _DARWIN_C_SOURCE 1 - -/* This must be set to 64 on some systems to enable large file support. */ -#define _FILE_OFFSET_BITS 64 - -/* Define to include mbstate_t for mbrtowc */ -/* #undef _INCLUDE__STDC_A1_SOURCE */ - -/* This must be defined on some systems to enable large file support. */ -#define _LARGEFILE_SOURCE 1 - -/* This must be defined on AIX systems to enable large file support. */ -/* #undef _LARGE_FILES */ - -/* Define on NetBSD to activate all library features */ -#define _NETBSD_SOURCE 1 - -/* Define to activate features from IEEE Stds 1003.1-2008 */ -/* #undef _POSIX_C_SOURCE */ - -/* Define if you have POSIX threads, and your system does not define that. */ -/* #undef _POSIX_THREADS */ - -/* framework name */ -#define _PYTHONFRAMEWORK "Python" - -/* Maximum length in bytes of a thread name */ -#define _PYTHREAD_NAME_MAXLEN 63 - -/* Defined if _Complex C type can be used with libffi. */ -/* #undef _Py_FFI_SUPPORT_C_COMPLEX */ - -/* HACL* library can compile SIMD128 implementations */ -/* #undef _Py_HACL_CAN_COMPILE_VEC128 */ - -/* HACL* library can compile SIMD256 implementations */ -/* #undef _Py_HACL_CAN_COMPILE_VEC256 */ - -/* Define to 1 if the machine stack grows down (default); 0 if it grows up. */ -#define _Py_STACK_GROWS_DOWN 1 - -/* Define to force use of thread-safe errno, h_errno, and other functions */ -#define _REENTRANT 1 - -/* Define to 1 if you want to emulate getpid() on WASI */ -/* #undef _WASI_EMULATED_GETPID */ - -/* Define to 1 if you want to emulate process clocks on WASI */ -/* #undef _WASI_EMULATED_PROCESS_CLOCKS */ - -/* Define to 1 if you want to emulate signals on WASI */ -/* #undef _WASI_EMULATED_SIGNAL */ - -/* Define to the level of X/Open that your system supports */ -/* #undef _XOPEN_SOURCE */ - -/* Define to activate Unix95-and-earlier features */ -/* #undef _XOPEN_SOURCE_EXTENDED */ - -/* Define on FreeBSD to activate all library features */ -#define __BSD_VISIBLE 1 - -/* Define to 'long' if does not define clock_t. */ -/* #undef clock_t */ - -/* Define to empty if 'const' does not conform to ANSI C. */ -/* #undef const */ - -/* Define as 'int' if doesn't define. */ -/* #undef gid_t */ - -/* Define to 'int' if does not define. */ -/* #undef mode_t */ - -/* Define to 'long int' if does not define. */ -/* #undef off_t */ - -/* Define as a signed integer type capable of holding a process identifier. */ -/* #undef pid_t */ - -/* Define to empty if the keyword does not work. */ -/* #undef signed */ - -/* Define as 'unsigned int' if doesn't define. */ -/* #undef size_t */ - -/* Define to 'int' if does not define. */ -/* #undef socklen_t */ - -/* Define as 'int' if doesn't define. */ -/* #undef uid_t */ - - -/* Define the macros needed if on a UnixWare 7.x system. */ -#if defined(__USLC__) && defined(__SCO_VERSION__) -#define STRICT_SYSV_CURSES /* Don't use ncurses extensions */ -#endif - -#endif /*Py_PYCONFIG_H*/ - diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-ios-x86_64.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-ios-x86_64.h deleted file mode 100644 index 6adb7b90c..000000000 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig-ios-x86_64.h +++ /dev/null @@ -1,2088 +0,0 @@ -/* pyconfig.h. Generated from pyconfig.h.in by configure. */ -/* pyconfig.h.in. Generated from configure.ac by autoheader. */ - - -#ifndef Py_PYCONFIG_H -#define Py_PYCONFIG_H - - -/* Define if building universal (internal helper macro) */ -/* #undef AC_APPLE_UNIVERSAL_BUILD */ - -/* BUILD_GNU_TYPE + AIX_BUILDDATE are used to construct the PEP425 tag of the - build system. */ -/* #undef AIX_BUILDDATE */ - -/* Define for AIX if your compiler is a genuine IBM xlC/xlC_r and you want - support for AIX C++ shared extension modules. */ -/* #undef AIX_GENUINE_CPLUSPLUS */ - -/* The normal alignment of 'long', in bytes. */ -#define ALIGNOF_LONG 8 - -/* The normal alignment of 'max_align_t', in bytes. */ -#define ALIGNOF_MAX_ALIGN_T 16 - -/* The normal alignment of 'size_t', in bytes. */ -#define ALIGNOF_SIZE_T 8 - -/* Alternative SOABI used in debug build to load C extensions built in release - mode */ -/* #undef ALT_SOABI */ - -/* The Android API level. */ -/* #undef ANDROID_API_LEVEL */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored in ARM - mixed-endian order (byte order 45670123) */ -/* #undef DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the most - significant byte first */ -/* #undef DOUBLE_IS_BIG_ENDIAN_IEEE754 */ - -/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the - least significant byte first */ -#define DOUBLE_IS_LITTLE_ENDIAN_IEEE754 1 - -/* Define if --enable-ipv6 is specified */ -#define ENABLE_IPV6 1 - -/* Define if getpgrp() must be called as getpgrp(0). */ -/* #undef GETPGRP_HAVE_ARG */ - -/* Define if you have the 'accept' function. */ -#define HAVE_ACCEPT 1 - -/* Define to 1 if you have the 'accept4' function. */ -/* #undef HAVE_ACCEPT4 */ - -/* Define to 1 if you have the 'acosh' function. */ -#define HAVE_ACOSH 1 - -/* struct addrinfo (netdb.h) */ -#define HAVE_ADDRINFO 1 - -/* Define to 1 if you have the 'alarm' function. */ -#define HAVE_ALARM 1 - -/* Define if aligned memory access is required */ -#define HAVE_ALIGNED_REQUIRED 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ALLOCA_H 1 - -/* Define this if your time.h defines altzone. */ -/* #undef HAVE_ALTZONE */ - -/* Define to 1 if you have the 'asinh' function. */ -#define HAVE_ASINH 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ASM_TYPES_H */ - -/* Define to 1 if you have the 'atanh' function. */ -#define HAVE_ATANH 1 - -/* Define to 1 if you have the 'backtrace' function. */ -#define HAVE_BACKTRACE 1 - -/* Define if you have the 'bind' function. */ -#define HAVE_BIND 1 - -/* Define to 1 if you have the 'bind_textdomain_codeset' function. */ -/* #undef HAVE_BIND_TEXTDOMAIN_CODESET */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_BLUETOOTH_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BLUETOOTH_H */ - -/* Define if mbstowcs(NULL, "text", 0) does not return the number of wide - chars that would be converted. */ -/* #undef HAVE_BROKEN_MBSTOWCS */ - -/* Define if nice() returns success/failure instead of the new priority. */ -/* #undef HAVE_BROKEN_NICE */ - -/* Define if the system reports an invalid PIPE_BUF value. */ -/* #undef HAVE_BROKEN_PIPE_BUF */ - -/* Define if poll() sets errno on invalid file descriptors. */ -/* #undef HAVE_BROKEN_POLL */ - -/* Define if the Posix semaphores do not work on your system */ -/* #undef HAVE_BROKEN_POSIX_SEMAPHORES */ - -/* Define if pthread_sigmask() does not work on your system. */ -/* #undef HAVE_BROKEN_PTHREAD_SIGMASK */ - -/* define to 1 if your sem_getvalue is broken. */ -#define HAVE_BROKEN_SEM_GETVALUE 1 - -/* Define if 'unsetenv' does not return an int. */ -/* #undef HAVE_BROKEN_UNSETENV */ - -/* Has builtin __atomic_load_n() and __atomic_store_n() functions */ -#define HAVE_BUILTIN_ATOMIC 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_BZLIB_H */ - -/* Define to 1 if you have the 'chflags' function. */ -#define HAVE_CHFLAGS 1 - -/* Define to 1 if you have the 'chmod' function. */ -#define HAVE_CHMOD 1 - -/* Define to 1 if you have the 'chown' function. */ -#define HAVE_CHOWN 1 - -/* Define if you have the 'chroot' function. */ -#define HAVE_CHROOT 1 - -/* Define to 1 if you have the 'clock' function. */ -#define HAVE_CLOCK 1 - -/* Define to 1 if you have the 'clock_getres' function. */ -#define HAVE_CLOCK_GETRES 1 - -/* Define to 1 if you have the 'clock_gettime' function. */ -#define HAVE_CLOCK_GETTIME 1 - -/* Define to 1 if you have the 'clock_nanosleep' function. */ -/* #undef HAVE_CLOCK_NANOSLEEP */ - -/* Define to 1 if you have the 'clock_settime' function. */ -/* #undef HAVE_CLOCK_SETTIME */ - -/* Define to 1 if the system has the type 'clock_t'. */ -#define HAVE_CLOCK_T 1 - -/* Define to 1 if you have the 'closefrom' function. */ -/* #undef HAVE_CLOSEFROM */ - -/* Define to 1 if you have the 'close_range' function. */ -/* #undef HAVE_CLOSE_RANGE */ - -/* Define if the C compiler supports computed gotos. */ -/* #undef HAVE_COMPUTED_GOTOS */ - -/* Define to 1 if you have the 'confstr' function. */ -#define HAVE_CONFSTR 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CONIO_H */ - -/* Define if you have the 'connect' function. */ -#define HAVE_CONNECT 1 - -/* Define to 1 if you have the 'copy_file_range' function. */ -/* #undef HAVE_COPY_FILE_RANGE */ - -/* Define to 1 if you have the 'ctermid' function. */ -#define HAVE_CTERMID 1 - -/* Define if you have the 'ctermid_r' function. */ -#define HAVE_CTERMID_R 1 - -/* Define if you have the 'filter' function. */ -/* #undef HAVE_CURSES_FILTER */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_CURSES_H */ - -/* Define if you have the 'has_key' function. */ -/* #undef HAVE_CURSES_HAS_KEY */ - -/* Define if you have the 'immedok' function. */ -/* #undef HAVE_CURSES_IMMEDOK */ - -/* Define if you have the 'is_pad' function. */ -/* #undef HAVE_CURSES_IS_PAD */ - -/* Define if you have the 'is_term_resized' function. */ -/* #undef HAVE_CURSES_IS_TERM_RESIZED */ - -/* Define if you have the 'resizeterm' function. */ -/* #undef HAVE_CURSES_RESIZETERM */ - -/* Define if you have the 'resize_term' function. */ -/* #undef HAVE_CURSES_RESIZE_TERM */ - -/* Define if you have the 'syncok' function. */ -/* #undef HAVE_CURSES_SYNCOK */ - -/* Define if you have the 'typeahead' function. */ -/* #undef HAVE_CURSES_TYPEAHEAD */ - -/* Define if you have the 'use_env' function. */ -/* #undef HAVE_CURSES_USE_ENV */ - -/* Define if you have the 'wchgat' function. */ -/* #undef HAVE_CURSES_WCHGAT */ - -/* Define to 1 if you have the header file. */ -#define HAVE_DB_H 1 - -/* Define to 1 if you have the declaration of 'RTLD_DEEPBIND', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_DEEPBIND 0 - -/* Define to 1 if you have the declaration of 'RTLD_GLOBAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_GLOBAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_LAZY', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LAZY 1 - -/* Define to 1 if you have the declaration of 'RTLD_LOCAL', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_LOCAL 1 - -/* Define to 1 if you have the declaration of 'RTLD_MEMBER', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_MEMBER 0 - -/* Define to 1 if you have the declaration of 'RTLD_NODELETE', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NODELETE 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOLOAD', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOLOAD 1 - -/* Define to 1 if you have the declaration of 'RTLD_NOW', and to 0 if you - don't. */ -#define HAVE_DECL_RTLD_NOW 1 - -/* Define to 1 if you have the declaration of 'tzname', and to 0 if you don't. - */ -/* #undef HAVE_DECL_TZNAME */ - -/* Define to 1 if you have the declaration of 'UT_NAMESIZE', and to 0 if you - don't. */ -#define HAVE_DECL_UT_NAMESIZE 0 - -/* Define to 1 if you have the device macros. */ -#define HAVE_DEVICE_MACROS 1 - -/* Define to 1 if you have the /dev/ptc device file. */ -/* #undef HAVE_DEV_PTC */ - -/* Define to 1 if you have the /dev/ptmx device file. */ -/* #undef HAVE_DEV_PTMX */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_DIRECT_H */ - -/* Define to 1 if the dirent structure has a d_type field */ -#define HAVE_DIRENT_D_TYPE 1 - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -#define HAVE_DIRENT_H 1 - -/* Define if you have the 'dirfd' function or macro. */ -#define HAVE_DIRFD 1 - -/* Define to 1 if you have the 'dladdr' function. */ -#define HAVE_DLADDR 1 - -/* Define to 1 if you have the 'dladdr1' function. */ -/* #undef HAVE_DLADDR1 */ - -/* Define to 1 if you have the header file. */ -#define HAVE_DLFCN_H 1 - -/* Define to 1 if you have the 'dlopen' function. */ -#define HAVE_DLOPEN 1 - -/* Define to 1 if you have the 'dup' function. */ -#define HAVE_DUP 1 - -/* Define to 1 if you have the 'dup2' function. */ -#define HAVE_DUP2 1 - -/* Define to 1 if you have the 'dup3' function. */ -/* #undef HAVE_DUP3 */ - -/* Define if you have the '_dyld_shared_cache_contains_path' function. */ -#define HAVE_DYLD_SHARED_CACHE_CONTAINS_PATH 1 - -/* Defined when any dynamic module loading is enabled. */ -#define HAVE_DYNAMIC_LOADING 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_EDITLINE_READLINE_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_ENDIAN_H 1 - -/* Define if you have the 'epoll_create' function. */ -/* #undef HAVE_EPOLL */ - -/* Define if you have the 'epoll_create1' function. */ -/* #undef HAVE_EPOLL_CREATE1 */ - -/* Define to 1 if you have the 'erf' function. */ -#define HAVE_ERF 1 - -/* Define to 1 if you have the 'erfc' function. */ -#define HAVE_ERFC 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ERRNO_H 1 - -/* Define if you have the 'eventfd' function. */ -/* #undef HAVE_EVENTFD */ - -/* Define to 1 if you have the header file. */ -#define HAVE_EXECINFO_H 1 - -/* Define to 1 if you have the 'execv' function. */ -#define HAVE_EXECV 1 - -/* Define to 1 if you have the 'explicit_bzero' function. */ -/* #undef HAVE_EXPLICIT_BZERO */ - -/* Define to 1 if you have the 'explicit_memset' function. */ -/* #undef HAVE_EXPLICIT_MEMSET */ - -/* Define to 1 if you have the 'expm1' function. */ -#define HAVE_EXPM1 1 - -/* Define to 1 if you have the 'faccessat' function. */ -#define HAVE_FACCESSAT 1 - -/* Define if you have the 'fchdir' function. */ -#define HAVE_FCHDIR 1 - -/* Define to 1 if you have the 'fchmod' function. */ -#define HAVE_FCHMOD 1 - -/* Define to 1 if you have the 'fchmodat' function. */ -#define HAVE_FCHMODAT 1 - -/* Define to 1 if you have the 'fchown' function. */ -#define HAVE_FCHOWN 1 - -/* Define to 1 if you have the 'fchownat' function. */ -#define HAVE_FCHOWNAT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_FCNTL_H 1 - -/* Define if you have the 'fdatasync' function. */ -/* #undef HAVE_FDATASYNC */ - -/* Define to 1 if you have the 'fdopendir' function. */ -#define HAVE_FDOPENDIR 1 - -/* Define to 1 if you have the 'fdwalk' function. */ -/* #undef HAVE_FDWALK */ - -/* Define to 1 if you have the 'fexecve' function. */ -/* #undef HAVE_FEXECVE */ - -/* Define if you have the 'ffi_closure_alloc' function. */ -#define HAVE_FFI_CLOSURE_ALLOC 1 - -/* Define if you have the 'ffi_prep_cif_var' function. */ -#define HAVE_FFI_PREP_CIF_VAR 1 - -/* Define if you have the 'ffi_prep_closure_loc' function. */ -#define HAVE_FFI_PREP_CLOSURE_LOC 1 - -/* Define to 1 if you have the 'flock' function. */ -#define HAVE_FLOCK 1 - -/* Define to 1 if you have the 'fork' function. */ -#define HAVE_FORK 1 - -/* Define to 1 if you have the 'fork1' function. */ -/* #undef HAVE_FORK1 */ - -/* Define to 1 if you have the 'forkpty' function. */ -#define HAVE_FORKPTY 1 - -/* Define to 1 if you have the 'fpathconf' function. */ -#define HAVE_FPATHCONF 1 - -/* Define to 1 if you have the 'fseek64' function. */ -/* #undef HAVE_FSEEK64 */ - -/* Define to 1 if you have the 'fseeko' function. */ -#define HAVE_FSEEKO 1 - -/* Define to 1 if you have the 'fstatat' function. */ -#define HAVE_FSTATAT 1 - -/* Define to 1 if you have the 'fstatvfs' function. */ -#define HAVE_FSTATVFS 1 - -/* Define if you have the 'fsync' function. */ -#define HAVE_FSYNC 1 - -/* Define to 1 if you have the 'ftell64' function. */ -/* #undef HAVE_FTELL64 */ - -/* Define to 1 if you have the 'ftello' function. */ -#define HAVE_FTELLO 1 - -/* Define to 1 if you have the 'ftime' function. */ -#define HAVE_FTIME 1 - -/* Define to 1 if you have the 'ftruncate' function. */ -#define HAVE_FTRUNCATE 1 - -/* Define to 1 if you have the 'futimens' function. */ -#define HAVE_FUTIMENS 1 - -/* Define to 1 if you have the 'futimes' function. */ -#define HAVE_FUTIMES 1 - -/* Define to 1 if you have the 'futimesat' function. */ -/* #undef HAVE_FUTIMESAT */ - -/* Define to 1 if you have the 'gai_strerror' function. */ -#define HAVE_GAI_STRERROR 1 - -/* Define if we can use gcc inline assembler to get and set mc68881 fpcr */ -/* #undef HAVE_GCC_ASM_FOR_MC68881 */ - -/* Define if we can use x64 gcc inline assembler */ -#define HAVE_GCC_ASM_FOR_X64 1 - -/* Define if we can use gcc inline assembler to get and set x87 control word - */ -#define HAVE_GCC_ASM_FOR_X87 1 - -/* Define if your compiler provides __uint128_t */ -#define HAVE_GCC_UINT128_T 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_DASH_NDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_GDBM_NDBM_H */ - -/* Define if you have the getaddrinfo function. */ -#define HAVE_GETADDRINFO 1 - -/* Define this if you have flockfile(), getc_unlocked(), and funlockfile() */ -#define HAVE_GETC_UNLOCKED 1 - -/* Define to 1 if you have the 'getegid' function. */ -#define HAVE_GETEGID 1 - -/* Define to 1 if you have the 'getentropy' function. */ -/* #undef HAVE_GETENTROPY */ - -/* Define to 1 if you have the 'geteuid' function. */ -#define HAVE_GETEUID 1 - -/* Define to 1 if you have the 'getgid' function. */ -#define HAVE_GETGID 1 - -/* Define to 1 if you have the 'getgrent' function. */ -#define HAVE_GETGRENT 1 - -/* Define to 1 if you have the 'getgrgid' function. */ -#define HAVE_GETGRGID 1 - -/* Define to 1 if you have the 'getgrgid_r' function. */ -#define HAVE_GETGRGID_R 1 - -/* Define to 1 if you have the 'getgrnam_r' function. */ -#define HAVE_GETGRNAM_R 1 - -/* Define to 1 if you have the 'getgrouplist' function. */ -#define HAVE_GETGROUPLIST 1 - -/* Define to 1 if you have the 'getgroups' function. */ -/* #undef HAVE_GETGROUPS */ - -/* Define if you have the 'gethostbyaddr' function. */ -#define HAVE_GETHOSTBYADDR 1 - -/* Define to 1 if you have the 'gethostbyname' function. */ -#define HAVE_GETHOSTBYNAME 1 - -/* Define this if you have some version of gethostbyname_r() */ -/* #undef HAVE_GETHOSTBYNAME_R */ - -/* Define this if you have the 3-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_3_ARG */ - -/* Define this if you have the 5-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_5_ARG */ - -/* Define this if you have the 6-arg version of gethostbyname_r(). */ -/* #undef HAVE_GETHOSTBYNAME_R_6_ARG */ - -/* Define to 1 if you have the 'gethostname' function. */ -#define HAVE_GETHOSTNAME 1 - -/* Define to 1 if you have the 'getitimer' function. */ -#define HAVE_GETITIMER 1 - -/* Define to 1 if you have the 'getloadavg' function. */ -#define HAVE_GETLOADAVG 1 - -/* Define to 1 if you have the 'getlogin' function. */ -#define HAVE_GETLOGIN 1 - -/* Define to 1 if you have the 'getlogin_r' function. */ -#define HAVE_GETLOGIN_R 1 - -/* Define to 1 if you have the 'getnameinfo' function. */ -#define HAVE_GETNAMEINFO 1 - -/* Define if you have the 'getpagesize' function. */ -#define HAVE_GETPAGESIZE 1 - -/* Define if you have the 'getpeername' function. */ -#define HAVE_GETPEERNAME 1 - -/* Define to 1 if you have the 'getpgid' function. */ -#define HAVE_GETPGID 1 - -/* Define to 1 if you have the 'getpgrp' function. */ -#define HAVE_GETPGRP 1 - -/* Define to 1 if you have the 'getpid' function. */ -#define HAVE_GETPID 1 - -/* Define to 1 if you have the 'getppid' function. */ -#define HAVE_GETPPID 1 - -/* Define to 1 if you have the 'getpriority' function. */ -#define HAVE_GETPRIORITY 1 - -/* Define if you have the 'getprotobyname' function. */ -#define HAVE_GETPROTOBYNAME 1 - -/* Define to 1 if you have the 'getpwent' function. */ -#define HAVE_GETPWENT 1 - -/* Define to 1 if you have the 'getpwnam_r' function. */ -#define HAVE_GETPWNAM_R 1 - -/* Define to 1 if you have the 'getpwuid' function. */ -#define HAVE_GETPWUID 1 - -/* Define to 1 if you have the 'getpwuid_r' function. */ -#define HAVE_GETPWUID_R 1 - -/* Define to 1 if the getrandom() function is available */ -/* #undef HAVE_GETRANDOM */ - -/* Define to 1 if the Linux getrandom() syscall is available */ -/* #undef HAVE_GETRANDOM_SYSCALL */ - -/* Define to 1 if you have the 'getresgid' function. */ -/* #undef HAVE_GETRESGID */ - -/* Define to 1 if you have the 'getresuid' function. */ -/* #undef HAVE_GETRESUID */ - -/* Define to 1 if you have the 'getrusage' function. */ -#define HAVE_GETRUSAGE 1 - -/* Define if you have the 'getservbyname' function. */ -#define HAVE_GETSERVBYNAME 1 - -/* Define if you have the 'getservbyport' function. */ -#define HAVE_GETSERVBYPORT 1 - -/* Define to 1 if you have the 'getsid' function. */ -#define HAVE_GETSID 1 - -/* Define if you have the 'getsockname' function. */ -#define HAVE_GETSOCKNAME 1 - -/* Define to 1 if you have the 'getspent' function. */ -/* #undef HAVE_GETSPENT */ - -/* Define to 1 if you have the 'getspnam' function. */ -/* #undef HAVE_GETSPNAM */ - -/* Define to 1 if you have the 'getuid' function. */ -#define HAVE_GETUID 1 - -/* Define to 1 if you have the 'getwd' function. */ -#define HAVE_GETWD 1 - -/* Define if glibc has incorrect _FORTIFY_SOURCE wrappers for memmove and - bcopy. */ -/* #undef HAVE_GLIBC_MEMMOVE_BUG */ - -/* Define to 1 if you have the 'grantpt' function. */ -#define HAVE_GRANTPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_GRP_H 1 - -/* Define if you have the 'hstrerror' function. */ -#define HAVE_HSTRERROR 1 - -/* Define this if you have le64toh() */ -/* #undef HAVE_HTOLE64 */ - -/* Define to 1 if you have the 'if_nameindex' function. */ -#define HAVE_IF_NAMEINDEX 1 - -/* Define if you have the 'inet_aton' function. */ -#define HAVE_INET_ATON 1 - -/* Define if you have the 'inet_ntoa' function. */ -#define HAVE_INET_NTOA 1 - -/* Define if you have the 'inet_pton' function. */ -#define HAVE_INET_PTON 1 - -/* Define to 1 if you have the 'initgroups' function. */ -#define HAVE_INITGROUPS 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_INTTYPES_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_IO_H */ - -/* Define if gcc has the ipa-pure-const bug. */ -/* #undef HAVE_IPA_PURE_CONST_BUG */ - -/* Define to 1 if you have the 'kill' function. */ -#define HAVE_KILL 1 - -/* Define to 1 if you have the 'killpg' function. */ -#define HAVE_KILLPG 1 - -/* Define if you have the 'kqueue' function. */ -#define HAVE_KQUEUE 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_LANGINFO_H 1 - -/* Defined to enable large file support when an off_t is bigger than a long - and long long is at least as big as an off_t. You may need to add some - flags for configuration and compilation to enable this mode. (For Solaris - and Linux, the necessary defines are already defined.) */ -/* #undef HAVE_LARGEFILE_SUPPORT */ - -/* Define to 1 if you have the 'lchflags' function. */ -#define HAVE_LCHFLAGS 1 - -/* Define to 1 if you have the 'lchmod' function. */ -#define HAVE_LCHMOD 1 - -/* Define to 1 if you have the 'lchown' function. */ -#define HAVE_LCHOWN 1 - -/* Define to 1 if you have the `db' library (-ldb). */ -/* #undef HAVE_LIBDB */ - -/* Define to 1 if you have the 'dl' library (-ldl). */ -#define HAVE_LIBDL 1 - -/* Define to 1 if you have the 'dld' library (-ldld). */ -/* #undef HAVE_LIBDLD */ - -/* Define to 1 if you have the 'ieee' library (-lieee). */ -/* #undef HAVE_LIBIEEE */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBINTL_H */ - -/* Define to 1 if you have the 'sendfile' library (-lsendfile). */ -/* #undef HAVE_LIBSENDFILE */ - -/* Define to 1 if you have the 'sqlite3' library (-lsqlite3). */ -#define HAVE_LIBSQLITE3 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LIBUTIL_H */ - -/* Define if you have the 'link' function. */ -#define HAVE_LINK 1 - -/* Define to 1 if you have the 'linkat' function. */ -#define HAVE_LINKAT 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_AUXVEC_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_BCM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_J1939_H */ - -/* Define if compiling using Linux 3.6 or later. */ -/* #undef HAVE_LINUX_CAN_RAW_FD_FRAMES */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_CAN_RAW_H */ - -/* Define if compiling using Linux 4.1 or later. */ -/* #undef HAVE_LINUX_CAN_RAW_JOIN_FILTERS */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_FS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_LIMITS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_MEMFD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_NETFILTER_IPV4_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_NETLINK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_QRTR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_RANDOM_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_SCHED_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_SOUNDCARD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_TIPC_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_VM_SOCKETS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LINUX_WAIT_H */ - -/* Define if you have the 'listen' function. */ -#define HAVE_LISTEN 1 - -/* Define to 1 if you have the 'lockf' function. */ -#define HAVE_LOCKF 1 - -/* Define to 1 if you have the 'log1p' function. */ -#define HAVE_LOG1P 1 - -/* Define to 1 if you have the 'log2' function. */ -#define HAVE_LOG2 1 - -/* Define to 1 if you have the `login_tty' function. */ -#define HAVE_LOGIN_TTY 1 - -/* Define to 1 if the system has the type 'long double'. */ -#define HAVE_LONG_DOUBLE 1 - -/* Define to 1 if you have the 'lstat' function. */ -#define HAVE_LSTAT 1 - -/* Define to 1 if you have the 'lutimes' function. */ -#define HAVE_LUTIMES 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_LZMA_H */ - -/* Define to 1 if you have the 'madvise' function. */ -#define HAVE_MADVISE 1 - -/* Define this if you have the makedev macro. */ -#define HAVE_MAKEDEV 1 - -/* Define if you have the 'MAXLOGNAME' constant. */ -#define HAVE_MAXLOGNAME 1 - -/* Define to 1 if you have the 'mbrtowc' function. */ -#define HAVE_MBRTOWC 1 - -/* Define if you have the 'memfd_create' function. */ -/* #undef HAVE_MEMFD_CREATE */ - -/* Define to 1 if you have the 'memrchr' function. */ -/* #undef HAVE_MEMRCHR */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_MINIX_CONFIG_H */ - -/* Define to 1 if you have the 'mkdirat' function. */ -#define HAVE_MKDIRAT 1 - -/* Define to 1 if you have the 'mkfifo' function. */ -#define HAVE_MKFIFO 1 - -/* Define to 1 if you have the 'mkfifoat' function. */ -#define HAVE_MKFIFOAT 1 - -/* Define to 1 if you have the 'mknod' function. */ -#define HAVE_MKNOD 1 - -/* Define to 1 if you have the 'mknodat' function. */ -#define HAVE_MKNODAT 1 - -/* Define to 1 if you have the 'mktime' function. */ -#define HAVE_MKTIME 1 - -/* Define to 1 if you have the 'mmap' function. */ -#define HAVE_MMAP 1 - -/* Define to 1 if you have the 'mremap' function. */ -/* #undef HAVE_MREMAP */ - -/* Define to 1 if you have the 'nanosleep' function. */ -#define HAVE_NANOSLEEP 1 - -/* Define if you have the 'ncurses' library */ -/* #undef HAVE_NCURSES */ - -/* Define if you have the 'ncursesw' library */ -/* #undef HAVE_NCURSESW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSESW_PANEL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_CURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_NCURSES_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NCURSES_PANEL_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NDBM_H 1 - -/* Define to 1 if you have the header file, and it defines 'DIR'. */ -/* #undef HAVE_NDIR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETCAN_CAN_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NETDB_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NETINET_IN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETLINK_NETLINK_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_NETPACKET_PACKET_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_ETHERNET_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_NET_IF_H 1 - -/* Define to 1 if you have the 'nice' function. */ -#define HAVE_NICE 1 - -/* Define if the internal form of wchar_t in non-Unicode locales is not - Unicode. */ -/* #undef HAVE_NON_UNICODE_WCHAR_T_REPRESENTATION */ - -/* Define to 1 if you have the 'openat' function. */ -#define HAVE_OPENAT 1 - -/* Define to 1 if you have the 'opendir' function. */ -#define HAVE_OPENDIR 1 - -/* Define to 1 if you have the 'openpty' function. */ -#define HAVE_OPENPTY 1 - -/* Define if you have the 'panel' library */ -/* #undef HAVE_PANEL */ - -/* Define if you have the 'panelw' library */ -/* #undef HAVE_PANELW */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PANEL_H */ - -/* Define to 1 if you have the 'pathconf' function. */ -#define HAVE_PATHCONF 1 - -/* Define to 1 if you have the 'pause' function. */ -#define HAVE_PAUSE 1 - -/* Define to 1 if you have the 'pipe' function. */ -#define HAVE_PIPE 1 - -/* Define to 1 if you have the 'pipe2' function. */ -/* #undef HAVE_PIPE2 */ - -/* Define to 1 if you have the 'plock' function. */ -/* #undef HAVE_PLOCK */ - -/* Define to 1 if you have the 'poll' function. */ -#define HAVE_POLL 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_POLL_H 1 - -/* Define to 1 if you have the 'posix_fadvise' function. */ -/* #undef HAVE_POSIX_FADVISE */ - -/* Define to 1 if you have the 'posix_fallocate' function. */ -/* #undef HAVE_POSIX_FALLOCATE */ - -/* Define to 1 if you have the 'posix_openpt' function. */ -#define HAVE_POSIX_OPENPT 1 - -/* Define to 1 if you have the 'posix_spawn' function. */ -#define HAVE_POSIX_SPAWN 1 - -/* Define to 1 if you have the 'posix_spawnp' function. */ -#define HAVE_POSIX_SPAWNP 1 - -/* Define to 1 if you have the 'posix_spawn_file_actions_addclosefrom_np' - function. */ -/* #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP */ - -/* Define to 1 if you have the 'pread' function. */ -#define HAVE_PREAD 1 - -/* Define to 1 if you have the 'preadv' function. */ -#define HAVE_PREADV 1 - -/* Define to 1 if you have the 'preadv2' function. */ -/* #undef HAVE_PREADV2 */ - -/* Define if you have the 'prlimit' function. */ -/* #undef HAVE_PRLIMIT */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PROCESS_H */ - -/* Define to 1 if you have the 'process_vm_readv' function. */ -/* #undef HAVE_PROCESS_VM_READV */ - -/* Define if your compiler supports function prototype */ -#define HAVE_PROTOTYPES 1 - -/* Define to 1 if you have the 'pthread_condattr_setclock' function. */ -/* #undef HAVE_PTHREAD_CONDATTR_SETCLOCK */ - -/* Define to 1 if you have the 'pthread_cond_timedwait_relative_np' function. - */ -#define HAVE_PTHREAD_COND_TIMEDWAIT_RELATIVE_NP 1 - -/* Defined for Solaris 2.6 bug in pthread header. */ -/* #undef HAVE_PTHREAD_DESTRUCTOR */ - -/* Define to 1 if you have the 'pthread_getattr_np' function. */ -/* #undef HAVE_PTHREAD_GETATTR_NP */ - -/* Define to 1 if you have the 'pthread_getcpuclockid' function. */ -/* #undef HAVE_PTHREAD_GETCPUCLOCKID */ - -/* Define to 1 if you have the 'pthread_getname_np' function. */ -#define HAVE_PTHREAD_GETNAME_NP 1 - -/* Define to 1 if you have the 'pthread_get_name_np' function. */ -/* #undef HAVE_PTHREAD_GET_NAME_NP */ - -/* Define to 1 if you have the header file. */ -#define HAVE_PTHREAD_H 1 - -/* Define to 1 if you have the 'pthread_init' function. */ -/* #undef HAVE_PTHREAD_INIT */ - -/* Define to 1 if you have the 'pthread_kill' function. */ -#define HAVE_PTHREAD_KILL 1 - -/* Define to 1 if you have the 'pthread_setname_np' function. */ -#define HAVE_PTHREAD_SETNAME_NP 1 - -/* Define to 1 if you have the 'pthread_set_name_np' function. */ -/* #undef HAVE_PTHREAD_SET_NAME_NP */ - -/* Define to 1 if you have the 'pthread_sigmask' function. */ -#define HAVE_PTHREAD_SIGMASK 1 - -/* Define if platform requires stubbed pthreads support */ -/* #undef HAVE_PTHREAD_STUBS */ - -/* Define to 1 if you have the 'ptsname' function. */ -#define HAVE_PTSNAME 1 - -/* Define to 1 if you have the 'ptsname_r' function. */ -#define HAVE_PTSNAME_R 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_PTY_H */ - -/* Define to 1 if you have the 'pwrite' function. */ -#define HAVE_PWRITE 1 - -/* Define to 1 if you have the 'pwritev' function. */ -#define HAVE_PWRITEV 1 - -/* Define to 1 if you have the 'pwritev2' function. */ -/* #undef HAVE_PWRITEV2 */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_READLINE_READLINE_H */ - -/* Define to 1 if you have the 'readlink' function. */ -#define HAVE_READLINK 1 - -/* Define to 1 if you have the 'readlinkat' function. */ -#define HAVE_READLINKAT 1 - -/* Define to 1 if you have the 'readv' function. */ -#define HAVE_READV 1 - -/* Define to 1 if you have the 'realpath' function. */ -#define HAVE_REALPATH 1 - -/* Define if you have the 'recvfrom' function. */ -#define HAVE_RECVFROM 1 - -/* Define to 1 if you have the 'renameat' function. */ -#define HAVE_RENAMEAT 1 - -/* Define if readline supports append_history */ -/* #undef HAVE_RL_APPEND_HISTORY */ - -/* Define if you can turn off readline's signal handling. */ -/* #undef HAVE_RL_CATCH_SIGNAL */ - -/* Define to 1 if the system has the type 'rl_compdisp_func_t'. */ -/* #undef HAVE_RL_COMPDISP_FUNC_T */ - -/* Define if you have readline 2.2 */ -/* #undef HAVE_RL_COMPLETION_APPEND_CHARACTER */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_COMPLETION_DISPLAY_MATCHES_HOOK */ - -/* Define if you have readline 4.2 */ -/* #undef HAVE_RL_COMPLETION_MATCHES */ - -/* Define if you have rl_completion_suppress_append */ -/* #undef HAVE_RL_COMPLETION_SUPPRESS_APPEND */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_PRE_INPUT_HOOK */ - -/* Define if you have readline 4.0 */ -/* #undef HAVE_RL_RESIZE_TERMINAL */ - -/* Define to 1 if you have the 'rtpSpawn' function. */ -/* #undef HAVE_RTPSPAWN */ - -/* Define to 1 if you have the 'sched_get_priority_max' function. */ -#define HAVE_SCHED_GET_PRIORITY_MAX 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SCHED_H 1 - -/* Define to 1 if you have the 'sched_rr_get_interval' function. */ -/* #undef HAVE_SCHED_RR_GET_INTERVAL */ - -/* Define to 1 if you have the 'sched_setaffinity' function. */ -/* #undef HAVE_SCHED_SETAFFINITY */ - -/* Define to 1 if you have the 'sched_setparam' function. */ -/* #undef HAVE_SCHED_SETPARAM */ - -/* Define to 1 if you have the 'sched_setscheduler' function. */ -/* #undef HAVE_SCHED_SETSCHEDULER */ - -/* Define to 1 if you have the 'sem_clockwait' function. */ -/* #undef HAVE_SEM_CLOCKWAIT */ - -/* Define to 1 if you have the 'sem_getvalue' function. */ -#define HAVE_SEM_GETVALUE 1 - -/* Define to 1 if you have the 'sem_open' function. */ -#define HAVE_SEM_OPEN 1 - -/* Define to 1 if you have the 'sem_timedwait' function. */ -/* #undef HAVE_SEM_TIMEDWAIT */ - -/* Define to 1 if you have the 'sem_unlink' function. */ -#define HAVE_SEM_UNLINK 1 - -/* Define to 1 if you have the 'sendfile' function. */ -#define HAVE_SENDFILE 1 - -/* Define if you have the 'sendto' function. */ -#define HAVE_SENDTO 1 - -/* Define to 1 if you have the 'setegid' function. */ -#define HAVE_SETEGID 1 - -/* Define to 1 if you have the 'seteuid' function. */ -#define HAVE_SETEUID 1 - -/* Define to 1 if you have the 'setgid' function. */ -#define HAVE_SETGID 1 - -/* Define if you have the 'setgroups' function. */ -#define HAVE_SETGROUPS 1 - -/* Define to 1 if you have the 'sethostname' function. */ -#define HAVE_SETHOSTNAME 1 - -/* Define to 1 if you have the 'setitimer' function. */ -#define HAVE_SETITIMER 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SETJMP_H 1 - -/* Define to 1 if you have the 'setlocale' function. */ -#define HAVE_SETLOCALE 1 - -/* Define to 1 if you have the 'setns' function. */ -/* #undef HAVE_SETNS */ - -/* Define to 1 if you have the 'setpgid' function. */ -#define HAVE_SETPGID 1 - -/* Define to 1 if you have the 'setpgrp' function. */ -#define HAVE_SETPGRP 1 - -/* Define to 1 if you have the 'setpriority' function. */ -#define HAVE_SETPRIORITY 1 - -/* Define to 1 if you have the 'setregid' function. */ -#define HAVE_SETREGID 1 - -/* Define to 1 if you have the 'setresgid' function. */ -/* #undef HAVE_SETRESGID */ - -/* Define to 1 if you have the 'setresuid' function. */ -/* #undef HAVE_SETRESUID */ - -/* Define to 1 if you have the 'setreuid' function. */ -#define HAVE_SETREUID 1 - -/* Define to 1 if you have the 'setsid' function. */ -#define HAVE_SETSID 1 - -/* Define if you have the 'setsockopt' function. */ -#define HAVE_SETSOCKOPT 1 - -/* Define to 1 if you have the 'setuid' function. */ -#define HAVE_SETUID 1 - -/* Define to 1 if you have the 'setvbuf' function. */ -#define HAVE_SETVBUF 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SHADOW_H */ - -/* Define to 1 if you have the 'shm_open' function. */ -#define HAVE_SHM_OPEN 1 - -/* Define to 1 if you have the 'shm_unlink' function. */ -#define HAVE_SHM_UNLINK 1 - -/* Define to 1 if you have the 'shutdown' function. */ -#define HAVE_SHUTDOWN 1 - -/* Define to 1 if you have the 'sigaction' function. */ -#define HAVE_SIGACTION 1 - -/* Define to 1 if you have the 'sigaltstack' function. */ -#define HAVE_SIGALTSTACK 1 - -/* Define to 1 if you have the 'sigfillset' function. */ -#define HAVE_SIGFILLSET 1 - -/* Define to 1 if 'si_band' is a member of 'siginfo_t'. */ -#define HAVE_SIGINFO_T_SI_BAND 1 - -/* Define to 1 if you have the 'siginterrupt' function. */ -#define HAVE_SIGINTERRUPT 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SIGNAL_H 1 - -/* Define to 1 if you have the 'sigpending' function. */ -#define HAVE_SIGPENDING 1 - -/* Define to 1 if you have the 'sigrelse' function. */ -#define HAVE_SIGRELSE 1 - -/* Define to 1 if you have the 'sigtimedwait' function. */ -/* #undef HAVE_SIGTIMEDWAIT */ - -/* Define to 1 if you have the 'sigwait' function. */ -#define HAVE_SIGWAIT 1 - -/* Define to 1 if you have the 'sigwaitinfo' function. */ -/* #undef HAVE_SIGWAITINFO */ - -/* Define to 1 if you have the 'snprintf' function. */ -#define HAVE_SNPRINTF 1 - -/* struct sockaddr_alg (linux/if_alg.h) */ -/* #undef HAVE_SOCKADDR_ALG */ - -/* Define if sockaddr has sa_len member */ -#define HAVE_SOCKADDR_SA_LEN 1 - -/* struct sockaddr_storage (sys/socket.h) */ -#define HAVE_SOCKADDR_STORAGE 1 - -/* Define if you have the 'socket' function. */ -#define HAVE_SOCKET 1 - -/* Define if you have the 'socketpair' function. */ -#define HAVE_SOCKETPAIR 1 - -/* Define to 1 if the system has the type 'socklen_t'. */ -#define HAVE_SOCKLEN_T 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SPAWN_H 1 - -/* Define to 1 if you have the 'splice' function. */ -/* #undef HAVE_SPLICE */ - -/* Define to 1 if the system has the type 'ssize_t'. */ -#define HAVE_SSIZE_T 1 - -/* Define to 1 if you have the 'statvfs' function. */ -#define HAVE_STATVFS 1 - -/* Define if you have struct stat.st_mtim.tv_nsec */ -/* #undef HAVE_STAT_TV_NSEC */ - -/* Define if you have struct stat.st_mtimensec */ -#define HAVE_STAT_TV_NSEC2 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDINT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STDLIB_H 1 - -/* Has stdatomic.h with atomic_int and atomic_uintptr_t */ -#define HAVE_STD_ATOMIC 1 - -/* Define to 1 if you have the 'strftime' function. */ -#define HAVE_STRFTIME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRINGS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_STRING_H 1 - -/* Define to 1 if you have the 'strlcpy' function. */ -#define HAVE_STRLCPY 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_STROPTS_H */ - -/* Define to 1 if you have the 'strsignal' function. */ -#define HAVE_STRSIGNAL 1 - -/* Define to 1 if 'pw_gecos' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_GECOS 1 - -/* Define to 1 if 'pw_passwd' is a member of 'struct passwd'. */ -#define HAVE_STRUCT_PASSWD_PW_PASSWD 1 - -/* Define to 1 if 'st_birthtime' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BIRTHTIME 1 - -/* Define to 1 if 'st_blksize' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLKSIZE 1 - -/* Define to 1 if 'st_blocks' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_BLOCKS 1 - -/* Define to 1 if 'st_flags' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_FLAGS 1 - -/* Define to 1 if 'st_gen' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_GEN 1 - -/* Define to 1 if 'st_rdev' is a member of 'struct stat'. */ -#define HAVE_STRUCT_STAT_ST_RDEV 1 - -/* Define to 1 if 'tm_zone' is a member of 'struct tm'. */ -#define HAVE_STRUCT_TM_TM_ZONE 1 - -/* Define if you have the 'symlink' function. */ -#define HAVE_SYMLINK 1 - -/* Define to 1 if you have the 'symlinkat' function. */ -#define HAVE_SYMLINKAT 1 - -/* Define to 1 if you have the 'sync' function. */ -#define HAVE_SYNC 1 - -/* Define to 1 if you have the 'sysconf' function. */ -#define HAVE_SYSCONF 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSEXITS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYSLOG_H 1 - -/* Define to 1 if you have the 'system' function. */ -/* #undef HAVE_SYSTEM */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_AUDIOIO_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_AUXV_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_BSDTTY_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_DEVPOLL_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_DIR_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_ENDIAN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_EPOLL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_EVENTFD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_EVENT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_FILE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_IOCTL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_KERN_CONTROL_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_LOADAVG_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_LOCK_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MEMFD_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MKDEV_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_MMAN_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_MODEM_H */ - -/* Define to 1 if you have the header file, and it defines 'DIR'. - */ -/* #undef HAVE_SYS_NDIR_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_PARAM_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_PIDFD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_POLL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_RANDOM_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_RESOURCE_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SELECT_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SENDFILE_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SOCKET_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SOUNDCARD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STATVFS_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_STAT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_SYSCALL_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYSMACROS_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_SYS_DOMAIN_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_TERMIO_H */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_SYS_TIMERFD_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIMES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TIME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_TYPES_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UIO_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UN_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_UTSNAME_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_WAIT_H 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_SYS_XATTR_H 1 - -/* Define to 1 if you have the 'tcgetpgrp' function. */ -#define HAVE_TCGETPGRP 1 - -/* Define to 1 if you have the 'tcsetpgrp' function. */ -#define HAVE_TCSETPGRP 1 - -/* Define to 1 if you have the 'tempnam' function. */ -#define HAVE_TEMPNAM 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_TERMIOS_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_TERM_H */ - -/* Define to 1 if you have the 'timegm' function. */ -#define HAVE_TIMEGM 1 - -/* Define if you have the 'timerfd_create' function. */ -/* #undef HAVE_TIMERFD_CREATE */ - -/* Define to 1 if you have the 'times' function. */ -#define HAVE_TIMES 1 - -/* Define to 1 if you have the 'tmpfile' function. */ -#define HAVE_TMPFILE 1 - -/* Define to 1 if you have the 'tmpnam' function. */ -#define HAVE_TMPNAM 1 - -/* Define to 1 if you have the 'tmpnam_r' function. */ -/* #undef HAVE_TMPNAM_R */ - -/* Define to 1 if your 'struct tm' has 'tm_zone'. Deprecated, use - 'HAVE_STRUCT_TM_TM_ZONE' instead. */ -#define HAVE_TM_ZONE 1 - -/* Define to 1 if you have the 'truncate' function. */ -#define HAVE_TRUNCATE 1 - -/* Define to 1 if you have the 'ttyname_r' function. */ -#define HAVE_TTYNAME_R 1 - -/* Define to 1 if you don't have 'tm_zone' but do have the external array - 'tzname'. */ -/* #undef HAVE_TZNAME */ - -/* Define to 1 if you have the 'umask' function. */ -#define HAVE_UMASK 1 - -/* Define to 1 if you have the 'uname' function. */ -#define HAVE_UNAME 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UNISTD_H 1 - -/* Define to 1 if you have the 'unlinkat' function. */ -#define HAVE_UNLINKAT 1 - -/* Define to 1 if you have the 'unlockpt' function. */ -#define HAVE_UNLOCKPT 1 - -/* Define to 1 if you have the 'unshare' function. */ -/* #undef HAVE_UNSHARE */ - -/* Define if you have a useable wchar_t type defined in wchar.h; useable means - wchar_t must be an unsigned type with at least 16 bits. (see - Include/unicodeobject.h). */ -/* #undef HAVE_USABLE_WCHAR_T */ - -/* Define to 1 if you have the header file. */ -#define HAVE_UTIL_H 1 - -/* Define to 1 if you have the 'utimensat' function. */ -#define HAVE_UTIMENSAT 1 - -/* Define to 1 if you have the 'utimes' function. */ -#define HAVE_UTIMES 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_UTIME_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UTMP_H */ - -/* Define if you have the 'HAVE_UT_NAMESIZE' constant. */ -/* #undef HAVE_UT_NAMESIZE */ - -/* Define to 1 if you have the 'uuid_create' function. */ -/* #undef HAVE_UUID_CREATE */ - -/* Define to 1 if you have the 'uuid_enc_be' function. */ -/* #undef HAVE_UUID_ENC_BE */ - -/* Define if uuid_generate_time_safe() exists. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE */ - -/* Define if uuid_generate_time_safe() is able to deduce a MAC address. */ -/* #undef HAVE_UUID_GENERATE_TIME_SAFE_STABLE_MAC */ - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_UUID_H */ - -/* Define to 1 if you have the header file. */ -#define HAVE_UUID_UUID_H 1 - -/* Define to 1 if you have the 'vfork' function. */ -#define HAVE_VFORK 1 - -/* Define to 1 if you have the 'wait' function. */ -#define HAVE_WAIT 1 - -/* Define to 1 if you have the 'wait3' function. */ -#define HAVE_WAIT3 1 - -/* Define to 1 if you have the 'wait4' function. */ -#define HAVE_WAIT4 1 - -/* Define to 1 if you have the 'waitid' function. */ -#define HAVE_WAITID 1 - -/* Define to 1 if you have the 'waitpid' function. */ -#define HAVE_WAITPID 1 - -/* Define if the compiler provides a wchar.h header file. */ -#define HAVE_WCHAR_H 1 - -/* Define to 1 if you have the 'wcscoll' function. */ -#define HAVE_WCSCOLL 1 - -/* Define to 1 if you have the 'wcsftime' function. */ -#define HAVE_WCSFTIME 1 - -/* Define to 1 if you have the 'wcsxfrm' function. */ -#define HAVE_WCSXFRM 1 - -/* Define to 1 if you have the 'wmemcmp' function. */ -#define HAVE_WMEMCMP 1 - -/* Define if tzset() actually switches the local timezone in a meaningful way. - */ -/* #undef HAVE_WORKING_TZSET */ - -/* Define to 1 if you have the 'writev' function. */ -#define HAVE_WRITEV 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZDICT_H */ - -/* Define if the zlib library has inflateCopy */ -#define HAVE_ZLIB_COPY 1 - -/* Define to 1 if you have the header file. */ -#define HAVE_ZLIB_H 1 - -/* Define to 1 if you have the header file. */ -/* #undef HAVE_ZSTD_H */ - -/* Define to 1 if you have the '_getpty' function. */ -/* #undef HAVE__GETPTY */ - -/* Define to 1 if the system has the type '__uint128_t'. */ -#define HAVE___UINT128_T 1 - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in . - */ -/* #undef MAJOR_IN_MKDEV */ - -/* Define to 1 if 'major', 'minor', and 'makedev' are declared in - . */ -/* #undef MAJOR_IN_SYSMACROS */ - -/* Define if mvwdelch in curses.h is an expression. */ -/* #undef MVWDELCH_IS_EXPRESSION */ - -/* Define to the address where bug reports for this package should be sent. */ -/* #undef PACKAGE_BUGREPORT */ - -/* Define to the full name of this package. */ -/* #undef PACKAGE_NAME */ - -/* Define to the full name and version of this package. */ -/* #undef PACKAGE_STRING */ - -/* Define to the one symbol short name of this package. */ -/* #undef PACKAGE_TARNAME */ - -/* Define to the home page for this package. */ -/* #undef PACKAGE_URL */ - -/* Define to the version of this package. */ -/* #undef PACKAGE_VERSION */ - -/* Define if POSIX semaphores aren't enabled on your system */ -/* #undef POSIX_SEMAPHORES_NOT_ENABLED */ - -/* Define if pthread_key_t is compatible with int. */ -/* #undef PTHREAD_KEY_T_IS_COMPATIBLE_WITH_INT */ - -/* Defined if PTHREAD_SCOPE_SYSTEM supported. */ -/* #undef PTHREAD_SYSTEM_SCHED_SUPPORTED */ - -/* Define as the preferred size in bits of long digits */ -/* #undef PYLONG_BITS_IN_DIGIT */ - -/* enabled builtin hash modules */ -#define PY_BUILTIN_HASHLIB_HASHES "md5,sha1,sha2,sha3,blake2" - -/* Define if you want to coerce the C locale to a UTF-8 based locale */ -#define PY_COERCE_C_LOCALE 1 - -/* Define to 1 if you have the perf trampoline. */ -/* #undef PY_HAVE_PERF_TRAMPOLINE */ - -/* Define to 1 to build the sqlite module with loadable extensions support. */ -/* #undef PY_SQLITE_ENABLE_LOAD_EXTENSION */ - -/* Define if SQLite was compiled with the serialize API */ -#define PY_SQLITE_HAVE_SERIALIZE 1 - -/* Default cipher suites list for ssl module. 1: Python's preferred selection, - 2: leave OpenSSL defaults untouched, 0: custom string */ -#define PY_SSL_DEFAULT_CIPHERS 1 - -/* Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0 */ -/* #undef PY_SSL_DEFAULT_CIPHER_STRING */ - -/* PEP 11 Support tier (1, 2, 3 or 0 for unsupported) */ -#define PY_SUPPORT_TIER 0 - -/* Define if you want to build an interpreter with many run-time checks. */ -/* #undef Py_DEBUG */ - -/* Defined if Python is built as a shared library. */ -/* #undef Py_ENABLE_SHARED */ - -/* Define if you want to disable the GIL */ -/* #undef Py_GIL_DISABLED */ - -/* Define hash algorithm for str, bytes and memoryview. SipHash24: 1, FNV: 2, - SipHash13: 3, externally defined: 0 */ -/* #undef Py_HASH_ALGORITHM */ - -/* Define if you want to enable remote debugging support. */ -#define Py_REMOTE_DEBUG 1 - -/* Define if rl_startup_hook takes arguments */ -/* #undef Py_RL_STARTUP_HOOK_TAKES_ARGS */ - -/* Define if you want to enable internal statistics gathering. */ -/* #undef Py_STATS */ - -/* The version of SunOS/Solaris as reported by `uname -r' without the dot. */ -/* #undef Py_SUNOS_VERSION */ - -/* Define if you want to use tail-calling interpreters in CPython. */ -/* #undef Py_TAIL_CALL_INTERP */ - -/* Define if you want to enable tracing references for debugging purpose */ -/* #undef Py_TRACE_REFS */ - -/* assume C89 semantics that RETSIGTYPE is always void */ -#define RETSIGTYPE void - -/* Define if setpgrp() must be called as setpgrp(0, 0). */ -/* #undef SETPGRP_HAVE_ARG */ - -/* Define if i>>j for signed int i does not extend the sign bit when i < 0 */ -/* #undef SIGNED_RIGHT_SHIFT_ZERO_FILLS */ - -/* The size of 'double', as computed by sizeof. */ -#define SIZEOF_DOUBLE 8 - -/* The size of 'float', as computed by sizeof. */ -#define SIZEOF_FLOAT 4 - -/* The size of 'fpos_t', as computed by sizeof. */ -#define SIZEOF_FPOS_T 8 - -/* The size of 'int', as computed by sizeof. */ -#define SIZEOF_INT 4 - -/* The size of 'long', as computed by sizeof. */ -#define SIZEOF_LONG 8 - -/* The size of 'long double', as computed by sizeof. */ -#define SIZEOF_LONG_DOUBLE 16 - -/* The size of 'long long', as computed by sizeof. */ -#define SIZEOF_LONG_LONG 8 - -/* The size of 'off_t', as computed by sizeof. */ -#define SIZEOF_OFF_T 8 - -/* The size of 'pid_t', as computed by sizeof. */ -#define SIZEOF_PID_T 4 - -/* The size of 'pthread_key_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_KEY_T 8 - -/* The size of 'pthread_t', as computed by sizeof. */ -#define SIZEOF_PTHREAD_T 8 - -/* The size of 'short', as computed by sizeof. */ -#define SIZEOF_SHORT 2 - -/* The size of 'size_t', as computed by sizeof. */ -#define SIZEOF_SIZE_T 8 - -/* The size of 'time_t', as computed by sizeof. */ -#define SIZEOF_TIME_T 8 - -/* The size of 'uintptr_t', as computed by sizeof. */ -#define SIZEOF_UINTPTR_T 8 - -/* The size of 'void *', as computed by sizeof. */ -#define SIZEOF_VOID_P 8 - -/* The size of 'wchar_t', as computed by sizeof. */ -#define SIZEOF_WCHAR_T 4 - -/* The size of '_Bool', as computed by sizeof. */ -#define SIZEOF__BOOL 1 - -/* Define to 1 if you have the ANSI C header files. */ -#define STDC_HEADERS 1 - -/* Define if you can safely include both and - (which you can't on SCO ODT 3.0). */ -#define SYS_SELECT_WITH_SYS_TIME 1 - -/* Custom thread stack size depending on chosen sanitizer runtimes. */ -#define THREAD_STACK_SIZE 0x1000000 - -/* Library needed by timemodule.c: librt may be needed for clock_gettime() */ -/* #undef TIMEMODULE_LIB */ - -/* Define to 1 if your declares 'struct tm'. */ -/* #undef TM_IN_SYS_TIME */ - -/* Define if you want to use computed gotos in ceval.c. */ -/* #undef USE_COMPUTED_GOTOS */ - -/* Enable extensions on AIX, Interix, z/OS. */ -#ifndef _ALL_SOURCE -# define _ALL_SOURCE 1 -#endif -/* Enable general extensions on macOS. */ -#ifndef _DARWIN_C_SOURCE -# define _DARWIN_C_SOURCE 1 -#endif -/* Enable general extensions on Solaris. */ -#ifndef __EXTENSIONS__ -# define __EXTENSIONS__ 1 -#endif -/* Enable GNU extensions on systems that have them. */ -#ifndef _GNU_SOURCE -# define _GNU_SOURCE 1 -#endif -/* Enable X/Open compliant socket functions that do not require linking - with -lxnet on HP-UX 11.11. */ -#ifndef _HPUX_ALT_XOPEN_SOCKET_API -# define _HPUX_ALT_XOPEN_SOCKET_API 1 -#endif -/* Identify the host operating system as Minix. - This macro does not affect the system headers' behavior. - A future release of Autoconf may stop defining this macro. */ -#ifndef _MINIX -/* # undef _MINIX */ -#endif -/* Enable general extensions on NetBSD. - Enable NetBSD compatibility extensions on Minix. */ -#ifndef _NETBSD_SOURCE -# define _NETBSD_SOURCE 1 -#endif -/* Enable OpenBSD compatibility extensions on NetBSD. - Oddly enough, this does nothing on OpenBSD. */ -#ifndef _OPENBSD_SOURCE -# define _OPENBSD_SOURCE 1 -#endif -/* Define to 1 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_SOURCE -/* # undef _POSIX_SOURCE */ -#endif -/* Define to 2 if needed for POSIX-compatible behavior. */ -#ifndef _POSIX_1_SOURCE -/* # undef _POSIX_1_SOURCE */ -#endif -/* Enable POSIX-compatible threading on Solaris. */ -#ifndef _POSIX_PTHREAD_SEMANTICS -# define _POSIX_PTHREAD_SEMANTICS 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-5:2014. */ -#ifndef __STDC_WANT_IEC_60559_ATTRIBS_EXT__ -# define __STDC_WANT_IEC_60559_ATTRIBS_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-1:2014. */ -#ifndef __STDC_WANT_IEC_60559_BFP_EXT__ -# define __STDC_WANT_IEC_60559_BFP_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-2:2015. */ -#ifndef __STDC_WANT_IEC_60559_DFP_EXT__ -# define __STDC_WANT_IEC_60559_DFP_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex F. */ -#ifndef __STDC_WANT_IEC_60559_EXT__ -# define __STDC_WANT_IEC_60559_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TS 18661-4:2015. */ -#ifndef __STDC_WANT_IEC_60559_FUNCS_EXT__ -# define __STDC_WANT_IEC_60559_FUNCS_EXT__ 1 -#endif -/* Enable extensions specified by C23 Annex H and ISO/IEC TS 18661-3:2015. */ -#ifndef __STDC_WANT_IEC_60559_TYPES_EXT__ -# define __STDC_WANT_IEC_60559_TYPES_EXT__ 1 -#endif -/* Enable extensions specified by ISO/IEC TR 24731-2:2010. */ -#ifndef __STDC_WANT_LIB_EXT2__ -# define __STDC_WANT_LIB_EXT2__ 1 -#endif -/* Enable extensions specified by ISO/IEC 24747:2009. */ -#ifndef __STDC_WANT_MATH_SPEC_FUNCS__ -# define __STDC_WANT_MATH_SPEC_FUNCS__ 1 -#endif -/* Enable extensions on HP NonStop. */ -#ifndef _TANDEM_SOURCE -# define _TANDEM_SOURCE 1 -#endif -/* Enable X/Open extensions. Define to 500 only if necessary - to make mbstate_t available. */ -#ifndef _XOPEN_SOURCE -/* # undef _XOPEN_SOURCE */ -#endif - - -/* Define if WINDOW in curses.h offers a field _flags. */ -/* #undef WINDOW_HAS_FLAGS */ - -/* Define if you want build the _decimal module using a coroutine-local rather - than a thread-local context */ -#define WITH_DECIMAL_CONTEXTVAR 1 - -/* Define if you want documentation strings in extension modules */ -#define WITH_DOC_STRINGS 1 - -/* Define if you want to compile in DTrace support */ -/* #undef WITH_DTRACE */ - -/* Define if you want to use the new-style (Openstep, Rhapsody, MacOS) dynamic - linker (dyld) instead of the old-style (NextStep) dynamic linker (rld). - Dyld is necessary to support frameworks. */ -/* #undef WITH_DYLD */ - -/* Define to build the readline module against libedit. */ -/* #undef WITH_EDITLINE */ - -/* Define to 1 if libintl is needed for locale functions. */ -/* #undef WITH_LIBINTL */ - -/* Define if you want to compile in mimalloc memory allocator. */ -#define WITH_MIMALLOC 1 - -/* Define if you want to produce an OpenStep/Rhapsody framework (shared - library plus accessory files). */ -#define WITH_NEXT_FRAMEWORK 1 - -/* Define if you want to compile in Python-specific mallocs */ -#define WITH_PYMALLOC 1 - -/* Define if you want pymalloc to be disabled when running under valgrind */ -/* #undef WITH_VALGRIND */ - -/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most - significant byte first (like Motorola and SPARC, unlike Intel). */ -#if defined AC_APPLE_UNIVERSAL_BUILD -# if defined __BIG_ENDIAN__ -# define WORDS_BIGENDIAN 1 -# endif -#else -# ifndef WORDS_BIGENDIAN -/* # undef WORDS_BIGENDIAN */ -# endif -#endif - -/* Define if arithmetic is subject to x87-style double rounding issue */ -/* #undef X87_DOUBLE_ROUNDING */ - -/* Define on OpenBSD to activate all library features */ -/* #undef _BSD_SOURCE */ - -/* Define on Darwin to activate all library features */ -#define _DARWIN_C_SOURCE 1 - -/* This must be set to 64 on some systems to enable large file support. */ -#define _FILE_OFFSET_BITS 64 - -/* Define to include mbstate_t for mbrtowc */ -/* #undef _INCLUDE__STDC_A1_SOURCE */ - -/* This must be defined on some systems to enable large file support. */ -#define _LARGEFILE_SOURCE 1 - -/* This must be defined on AIX systems to enable large file support. */ -/* #undef _LARGE_FILES */ - -/* Define on NetBSD to activate all library features */ -#define _NETBSD_SOURCE 1 - -/* Define to activate features from IEEE Stds 1003.1-2008 */ -/* #undef _POSIX_C_SOURCE */ - -/* Define if you have POSIX threads, and your system does not define that. */ -/* #undef _POSIX_THREADS */ - -/* framework name */ -#define _PYTHONFRAMEWORK "Python" - -/* Maximum length in bytes of a thread name */ -#define _PYTHREAD_NAME_MAXLEN 63 - -/* Defined if _Complex C type can be used with libffi. */ -/* #undef _Py_FFI_SUPPORT_C_COMPLEX */ - -/* HACL* library can compile SIMD128 implementations */ -#define _Py_HACL_CAN_COMPILE_VEC128 1 - -/* HACL* library can compile SIMD256 implementations */ -#define _Py_HACL_CAN_COMPILE_VEC256 1 - -/* Define to 1 if the machine stack grows down (default); 0 if it grows up. */ -#define _Py_STACK_GROWS_DOWN 1 - -/* Define to force use of thread-safe errno, h_errno, and other functions */ -#define _REENTRANT 1 - -/* Define to 1 if you want to emulate getpid() on WASI */ -/* #undef _WASI_EMULATED_GETPID */ - -/* Define to 1 if you want to emulate process clocks on WASI */ -/* #undef _WASI_EMULATED_PROCESS_CLOCKS */ - -/* Define to 1 if you want to emulate signals on WASI */ -/* #undef _WASI_EMULATED_SIGNAL */ - -/* Define to the level of X/Open that your system supports */ -/* #undef _XOPEN_SOURCE */ - -/* Define to activate Unix95-and-earlier features */ -/* #undef _XOPEN_SOURCE_EXTENDED */ - -/* Define on FreeBSD to activate all library features */ -#define __BSD_VISIBLE 1 - -/* Define to 'long' if does not define clock_t. */ -/* #undef clock_t */ - -/* Define to empty if 'const' does not conform to ANSI C. */ -/* #undef const */ - -/* Define as 'int' if doesn't define. */ -/* #undef gid_t */ - -/* Define to 'int' if does not define. */ -/* #undef mode_t */ - -/* Define to 'long int' if does not define. */ -/* #undef off_t */ - -/* Define as a signed integer type capable of holding a process identifier. */ -/* #undef pid_t */ - -/* Define to empty if the keyword does not work. */ -/* #undef signed */ - -/* Define as 'unsigned int' if doesn't define. */ -/* #undef size_t */ - -/* Define to 'int' if does not define. */ -/* #undef socklen_t */ - -/* Define as 'int' if doesn't define. */ -/* #undef uid_t */ - - -/* Define the macros needed if on a UnixWare 7.x system. */ -#if defined(__USLC__) && defined(__SCO_VERSION__) -#define STRICT_SYSV_CURSES /* Don't use ncurses extensions */ -#endif - -#endif /*Py_PYCONFIG_H*/ - diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig.h index 03611a820..c585835d3 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pyconfig.h @@ -1,19 +1,2169 @@ -#ifdef __APPLE__ - #ifdef __arm64__ - #include "pyconfig-ios-arm64.h" - #endif +/* pyconfig.h. Generated from pyconfig.h.in by configure. */ +/* pyconfig.h.in. Generated from configure.ac by autoheader. */ - #ifdef __x86_64__ - #include "pyconfig-ios-x86_64.h" - #endif + +#ifndef Py_PYCONFIG_H +#define Py_PYCONFIG_H + + +/* Define if building universal (internal helper macro) */ +/* #undef AC_APPLE_UNIVERSAL_BUILD */ + +/* BUILD_GNU_TYPE + AIX_BUILDDATE are used to construct the PEP425 tag of the + build system. */ +/* #undef AIX_BUILDDATE */ + +/* Define for AIX if your compiler is a genuine IBM xlC/xlC_r and you want + support for AIX C++ shared extension modules. */ +/* #undef AIX_GENUINE_CPLUSPLUS */ + +/* The normal alignment of 'long', in bytes. */ +#define ALIGNOF_LONG 8 + +/* The normal alignment of 'max_align_t', in bytes. */ +#define ALIGNOF_MAX_ALIGN_T 16 + +/* The normal alignment of 'size_t', in bytes. */ +#define ALIGNOF_SIZE_T 8 + +/* Alternative SOABI used in debug build to load C extensions built in release + mode */ +/* #undef ALT_SOABI */ + +/* The Android API level. */ +#define ANDROID_API_LEVEL 24 + +/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the most + significant byte first */ +/* #undef DOUBLE_IS_BIG_ENDIAN_IEEE754 */ + +/* Define if C doubles are 64-bit IEEE 754 binary format, stored with the + least significant byte first */ +#define DOUBLE_IS_LITTLE_ENDIAN_IEEE754 1 + +/* Define if --enable-ipv6 is specified */ +#define ENABLE_IPV6 1 + +/* Define if getpgrp() must be called as getpgrp(0). */ +/* #undef GETPGRP_HAVE_ARG */ + +/* Define if you have the 'accept' function. */ +#define HAVE_ACCEPT 1 + +/* Define to 1 if you have the 'accept4' function. */ +#define HAVE_ACCEPT4 1 + +/* Define to 1 if you have the 'acosh' function. */ +#define HAVE_ACOSH 1 + +/* struct addrinfo (netdb.h) */ +#define HAVE_ADDRINFO 1 + +/* Define to 1 if you have the 'alarm' function. */ +#define HAVE_ALARM 1 + +/* Define if aligned memory access is required */ +/* #undef HAVE_ALIGNED_REQUIRED */ + +/* Define to 1 if you have the header file. */ +#define HAVE_ALLOCA_H 1 + +/* Define this if your time.h defines altzone. */ +/* #undef HAVE_ALTZONE */ + +/* Define to 1 if you have the 'asinh' function. */ +#define HAVE_ASINH 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ASM_TYPES_H 1 + +/* Define to 1 if you have the 'atanh' function. */ +#define HAVE_ATANH 1 + +/* Define to 1 if you have the 'backtrace' function. */ +/* #undef HAVE_BACKTRACE */ + +/* Define if you have the 'bind' function. */ +#define HAVE_BIND 1 + +/* Define to 1 if you have the 'bind_textdomain_codeset' function. */ +/* #undef HAVE_BIND_TEXTDOMAIN_CODESET */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_BLUETOOTH_BLUETOOTH_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_BLUETOOTH_H */ + +/* Define if mbstowcs(NULL, "text", 0) does not return the number of wide + chars that would be converted. */ +/* #undef HAVE_BROKEN_MBSTOWCS */ + +/* Define if nice() returns success/failure instead of the new priority. */ +/* #undef HAVE_BROKEN_NICE */ + +/* Define if the system reports an invalid PIPE_BUF value. */ +/* #undef HAVE_BROKEN_PIPE_BUF */ + +/* Define if poll() sets errno on invalid file descriptors. */ +/* #undef HAVE_BROKEN_POLL */ + +/* Define if the Posix semaphores do not work on your system */ +/* #undef HAVE_BROKEN_POSIX_SEMAPHORES */ + +/* Define if pthread_sigmask() does not work on your system. */ +/* #undef HAVE_BROKEN_PTHREAD_SIGMASK */ + +/* define to 1 if your sem_getvalue is broken. */ +#define HAVE_BROKEN_SEM_GETVALUE 1 + +/* Define if 'unsetenv' does not return an int. */ +/* #undef HAVE_BROKEN_UNSETENV */ + +/* Has builtin __atomic_load_n() and __atomic_store_n() functions */ +#define HAVE_BUILTIN_ATOMIC 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_BZLIB_H 1 + +/* Define to 1 if you have the 'chflags' function. */ +/* #undef HAVE_CHFLAGS */ + +/* Define to 1 if you have the 'chmod' function. */ +#define HAVE_CHMOD 1 + +/* Define to 1 if you have the 'chown' function. */ +#define HAVE_CHOWN 1 + +/* Define if you have the 'chroot' function. */ +/* #undef HAVE_CHROOT */ + +/* Define to 1 if you have the 'clearenv' function. */ +#define HAVE_CLEARENV 1 + +/* Define to 1 if you have the 'clock' function. */ +#define HAVE_CLOCK 1 + +/* Define to 1 if you have the 'clock_getres' function. */ +#define HAVE_CLOCK_GETRES 1 + +/* Define to 1 if you have the 'clock_gettime' function. */ +#define HAVE_CLOCK_GETTIME 1 + +/* Define to 1 if you have the 'clock_nanosleep' function. */ +#define HAVE_CLOCK_NANOSLEEP 1 + +/* Define to 1 if you have the 'clock_settime' function. */ +/* #undef HAVE_CLOCK_SETTIME */ + +/* Define to 1 if the system has the type 'clock_t'. */ +#define HAVE_CLOCK_T 1 + +/* Define to 1 if you have the 'closefrom' function. */ +/* #undef HAVE_CLOSEFROM */ + +/* Define to 1 if you have the 'close_range' function. */ +/* #undef HAVE_CLOSE_RANGE */ + +/* Define if the C compiler supports computed gotos. */ +/* #undef HAVE_COMPUTED_GOTOS */ + +/* Define to 1 if you have the 'confstr' function. */ +/* #undef HAVE_CONFSTR */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_CONIO_H */ + +/* Define if you have the 'connect' function. */ +#define HAVE_CONNECT 1 + +/* Define to 1 if you have the 'copy_file_range' function. */ +/* #undef HAVE_COPY_FILE_RANGE */ + +/* Define to 1 if you have the 'ctermid' function. */ +/* #undef HAVE_CTERMID */ + +/* Define if you have the 'ctermid_r' function. */ +/* #undef HAVE_CTERMID_R */ + +/* Define if you have the 'ESCDELAY' variable. */ +/* #undef HAVE_CURSES_ESCDELAY */ + +/* Define if you have the 'filter' function. */ +/* #undef HAVE_CURSES_FILTER */ + +/* Define if you have the 'getmouse' function with the X/Open signature. */ +/* #undef HAVE_CURSES_GETMOUSE */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_CURSES_H */ + +/* Define if you have the 'has_key' function. */ +/* #undef HAVE_CURSES_HAS_KEY */ + +/* Define if you have the 'immedok' function. */ +/* #undef HAVE_CURSES_IMMEDOK */ + +/* Define if you have the 'is_pad' function. */ +/* #undef HAVE_CURSES_IS_PAD */ + +/* Define if you have the 'is_term_resized' function. */ +/* #undef HAVE_CURSES_IS_TERM_RESIZED */ + +/* Define if you have the 'resizeterm' function. */ +/* #undef HAVE_CURSES_RESIZETERM */ + +/* Define if you have the 'resize_term' function. */ +/* #undef HAVE_CURSES_RESIZE_TERM */ + +/* Define if you have the 'set_escdelay' function. */ +/* #undef HAVE_CURSES_SET_ESCDELAY */ + +/* Define if you have the 'set_tabsize' function. */ +/* #undef HAVE_CURSES_SET_TABSIZE */ + +/* Define if you have the 'syncok' function. */ +/* #undef HAVE_CURSES_SYNCOK */ + +/* Define if you have the 'TABSIZE' variable. */ +/* #undef HAVE_CURSES_TABSIZE */ + +/* Define if you have the 'typeahead' function. */ +/* #undef HAVE_CURSES_TYPEAHEAD */ + +/* Define if you have the 'use_env' function. */ +/* #undef HAVE_CURSES_USE_ENV */ + +/* Define if you have the 'wchgat' function. */ +/* #undef HAVE_CURSES_WCHGAT */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_DB_H */ + +/* Define to 1 if you have the declaration of 'PR_SET_VMA_ANON_NAME', and to 0 + if you don't. */ +#define HAVE_DECL_PR_SET_VMA_ANON_NAME 1 + +/* Define to 1 if you have the declaration of 'RTLD_DEEPBIND', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_DEEPBIND 0 + +/* Define to 1 if you have the declaration of 'RTLD_GLOBAL', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_GLOBAL 1 + +/* Define to 1 if you have the declaration of 'RTLD_LAZY', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_LAZY 1 + +/* Define to 1 if you have the declaration of 'RTLD_LOCAL', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_LOCAL 1 + +/* Define to 1 if you have the declaration of 'RTLD_MEMBER', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_MEMBER 0 + +/* Define to 1 if you have the declaration of 'RTLD_NODELETE', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_NODELETE 1 + +/* Define to 1 if you have the declaration of 'RTLD_NOLOAD', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_NOLOAD 1 + +/* Define to 1 if you have the declaration of 'RTLD_NOW', and to 0 if you + don't. */ +#define HAVE_DECL_RTLD_NOW 1 + +/* Define to 1 if you have the declaration of 'tzname', and to 0 if you don't. + */ +/* #undef HAVE_DECL_TZNAME */ + +/* Define to 1 if you have the declaration of 'UT_NAMESIZE', and to 0 if you + don't. */ +#define HAVE_DECL_UT_NAMESIZE 1 + +/* Define to 1 if you have the device macros. */ +#define HAVE_DEVICE_MACROS 1 + +/* Define to 1 if you have the /dev/ptc device file. */ +/* #undef HAVE_DEV_PTC */ + +/* Define to 1 if you have the /dev/ptmx device file. */ +/* #undef HAVE_DEV_PTMX */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_DIRECT_H */ + +/* Define to 1 if the dirent structure has a d_type field */ +#define HAVE_DIRENT_D_TYPE 1 + +/* Define to 1 if you have the header file, and it defines 'DIR'. + */ +#define HAVE_DIRENT_H 1 + +/* Define if you have the 'dirfd' function or macro. */ +#define HAVE_DIRFD 1 + +/* Define to 1 if you have the 'dladdr' function. */ +#define HAVE_DLADDR 1 + +/* Define to 1 if you have the 'dladdr1' function. */ +/* #undef HAVE_DLADDR1 */ + +/* Define to 1 if you have the header file. */ +#define HAVE_DLFCN_H 1 + +/* Define to 1 if you have the 'dlopen' function. */ +#define HAVE_DLOPEN 1 + +/* Define to 1 if you have the 'dl_iterate_phdr' function. */ +#define HAVE_DL_ITERATE_PHDR 1 + +/* Define to 1 if you have the 'dup' function. */ +#define HAVE_DUP 1 + +/* Define to 1 if you have the 'dup2' function. */ +#define HAVE_DUP2 1 + +/* Define to 1 if you have the 'dup3' function. */ +#define HAVE_DUP3 1 + +/* Define if you have the '_dyld_shared_cache_contains_path' function. */ +/* #undef HAVE_DYLD_SHARED_CACHE_CONTAINS_PATH */ + +/* Defined when any dynamic module loading is enabled. */ +#define HAVE_DYNAMIC_LOADING 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_EDITLINE_READLINE_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_ENDIAN_H 1 + +/* Define if you have the 'epoll_create' function. */ +#define HAVE_EPOLL 1 + +/* Define if you have the 'epoll_create1' function. */ +#define HAVE_EPOLL_CREATE1 1 + +/* Define to 1 if you have the 'erf' function. */ +#define HAVE_ERF 1 + +/* Define to 1 if you have the 'erfc' function. */ +#define HAVE_ERFC 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ERRNO_H 1 + +/* Define if you have the 'eventfd' function. */ +#define HAVE_EVENTFD 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_EXECINFO_H 1 + +/* Define to 1 if you have the 'execv' function. */ +#define HAVE_EXECV 1 + +/* Define to 1 if you have the 'explicit_bzero' function. */ +/* #undef HAVE_EXPLICIT_BZERO */ + +/* Define to 1 if you have the 'explicit_memset' function. */ +/* #undef HAVE_EXPLICIT_MEMSET */ + +/* Define to 1 if you have the 'expm1' function. */ +#define HAVE_EXPM1 1 + +/* Define to 1 if you have the 'faccessat' function. */ +#define HAVE_FACCESSAT 1 + +/* Define if you have the 'fchdir' function. */ +#define HAVE_FCHDIR 1 + +/* Define to 1 if you have the 'fchmod' function. */ +#define HAVE_FCHMOD 1 + +/* Define to 1 if you have the 'fchmodat' function. */ +#define HAVE_FCHMODAT 1 + +/* Define to 1 if you have the 'fchown' function. */ +#define HAVE_FCHOWN 1 + +/* Define to 1 if you have the 'fchownat' function. */ +#define HAVE_FCHOWNAT 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_FCNTL_H 1 + +/* Define if you have the 'fdatasync' function. */ +#define HAVE_FDATASYNC 1 + +/* Define to 1 if you have the 'fdopendir' function. */ +#define HAVE_FDOPENDIR 1 + +/* Define to 1 if you have the 'fdwalk' function. */ +/* #undef HAVE_FDWALK */ + +/* Define to 1 if you have the 'fexecve' function. */ +/* #undef HAVE_FEXECVE */ + +/* Define if you have the 'ffi_closure_alloc' function. */ +#define HAVE_FFI_CLOSURE_ALLOC 1 + +/* Define if you have the 'ffi_prep_cif_var' function. */ +#define HAVE_FFI_PREP_CIF_VAR 1 + +/* Define if you have the 'ffi_prep_closure_loc' function. */ +#define HAVE_FFI_PREP_CLOSURE_LOC 1 + +/* Define to 1 if you have the 'flock' function. */ +#define HAVE_FLOCK 1 + +/* Define to 1 if you have the 'fork' function. */ +#define HAVE_FORK 1 + +/* Define to 1 if you have the 'fork1' function. */ +/* #undef HAVE_FORK1 */ + +/* Define to 1 if you have the 'forkpty' function. */ +#define HAVE_FORKPTY 1 + +/* Define to 1 if you have the 'fpathconf' function. */ +#define HAVE_FPATHCONF 1 + +/* Define to 1 if you have the 'fseek64' function. */ +/* #undef HAVE_FSEEK64 */ + +/* Define to 1 if you have the 'fseeko' function. */ +#define HAVE_FSEEKO 1 + +/* Define to 1 if you have the 'fstatat' function. */ +#define HAVE_FSTATAT 1 + +/* Define to 1 if you have the 'fstatvfs' function. */ +#define HAVE_FSTATVFS 1 + +/* Define if you have the 'fsync' function. */ +#define HAVE_FSYNC 1 + +/* Define to 1 if you have the 'ftell64' function. */ +/* #undef HAVE_FTELL64 */ + +/* Define to 1 if you have the 'ftello' function. */ +#define HAVE_FTELLO 1 + +/* Define to 1 if you have the 'ftime' function. */ +/* #undef HAVE_FTIME */ + +/* Define to 1 if you have the 'ftruncate' function. */ +#define HAVE_FTRUNCATE 1 + +/* Define to 1 if you have the 'futimens' function. */ +#define HAVE_FUTIMENS 1 + +/* Define to 1 if you have the 'futimes' function. */ +/* #undef HAVE_FUTIMES */ + +/* Define to 1 if you have the 'futimesat' function. */ +/* #undef HAVE_FUTIMESAT */ + +/* Define to 1 if you have the 'gai_strerror' function. */ +#define HAVE_GAI_STRERROR 1 + +/* Define if we can use gcc inline assembler to get and set mc68881 fpcr */ +/* #undef HAVE_GCC_ASM_FOR_MC68881 */ + +/* Define if we can use x64 gcc inline assembler */ +/* #undef HAVE_GCC_ASM_FOR_X64 */ + +/* Define if we can use gcc inline assembler to get and set x87 control word + */ +/* #undef HAVE_GCC_ASM_FOR_X87 */ + +/* Define if your compiler provides __uint128_t */ +#define HAVE_GCC_UINT128_T 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_GDBM_DASH_NDBM_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_GDBM_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_GDBM_NDBM_H */ + +/* Define if you have the getaddrinfo function. */ +#define HAVE_GETADDRINFO 1 + +/* Define this if you have flockfile(), getc_unlocked(), and funlockfile() */ +#define HAVE_GETC_UNLOCKED 1 + +/* Define to 1 if you have the 'getegid' function. */ +#define HAVE_GETEGID 1 + +/* Define to 1 if you have the 'getentropy' function. */ +/* #undef HAVE_GETENTROPY */ + +/* Define to 1 if you have the 'geteuid' function. */ +#define HAVE_GETEUID 1 + +/* Define to 1 if you have the 'getgid' function. */ +#define HAVE_GETGID 1 + +/* Define to 1 if you have the 'getgrent' function. */ +/* #undef HAVE_GETGRENT */ + +/* Define to 1 if you have the 'getgrgid' function. */ +#define HAVE_GETGRGID 1 + +/* Define to 1 if you have the 'getgrgid_r' function. */ +#define HAVE_GETGRGID_R 1 + +/* Define to 1 if you have the 'getgrnam_r' function. */ +#define HAVE_GETGRNAM_R 1 + +/* Define to 1 if you have the 'getgrouplist' function. */ +#define HAVE_GETGROUPLIST 1 + +/* Define to 1 if you have the 'getgroups' function. */ +#define HAVE_GETGROUPS 1 + +/* Define if you have the 'gethostbyaddr' function. */ +#define HAVE_GETHOSTBYADDR 1 + +/* Define to 1 if you have the 'gethostbyname' function. */ +#define HAVE_GETHOSTBYNAME 1 + +/* Define this if you have some version of gethostbyname_r() */ +#define HAVE_GETHOSTBYNAME_R 1 + +/* Define this if you have the 3-arg version of gethostbyname_r(). */ +/* #undef HAVE_GETHOSTBYNAME_R_3_ARG */ + +/* Define this if you have the 5-arg version of gethostbyname_r(). */ +/* #undef HAVE_GETHOSTBYNAME_R_5_ARG */ + +/* Define this if you have the 6-arg version of gethostbyname_r(). */ +#define HAVE_GETHOSTBYNAME_R_6_ARG 1 + +/* Define to 1 if you have the 'gethostname' function. */ +#define HAVE_GETHOSTNAME 1 + +/* Define to 1 if you have the 'getitimer' function. */ +#define HAVE_GETITIMER 1 + +/* Define to 1 if you have the 'getloadavg' function. */ +/* #undef HAVE_GETLOADAVG */ + +/* Define to 1 if you have the 'getlogin' function. */ +#define HAVE_GETLOGIN 1 + +/* Define to 1 if you have the 'getlogin_r' function. */ +/* #undef HAVE_GETLOGIN_R */ + +/* Define to 1 if you have the 'getnameinfo' function. */ +#define HAVE_GETNAMEINFO 1 + +/* Define if you have the 'getpagesize' function. */ +#define HAVE_GETPAGESIZE 1 + +/* Define if you have the 'getpeername' function. */ +#define HAVE_GETPEERNAME 1 + +/* Define to 1 if you have the 'getpgid' function. */ +#define HAVE_GETPGID 1 + +/* Define to 1 if you have the 'getpgrp' function. */ +#define HAVE_GETPGRP 1 + +/* Define to 1 if you have the 'getpid' function. */ +#define HAVE_GETPID 1 + +/* Define to 1 if you have the 'getppid' function. */ +#define HAVE_GETPPID 1 + +/* Define to 1 if you have the 'getpriority' function. */ +#define HAVE_GETPRIORITY 1 + +/* Define if you have the 'getprotobyname' function. */ +#define HAVE_GETPROTOBYNAME 1 + +/* Define to 1 if you have the 'getpwent' function. */ +/* #undef HAVE_GETPWENT */ + +/* Define to 1 if you have the 'getpwnam_r' function. */ +#define HAVE_GETPWNAM_R 1 + +/* Define to 1 if you have the 'getpwuid' function. */ +#define HAVE_GETPWUID 1 + +/* Define to 1 if you have the 'getpwuid_r' function. */ +#define HAVE_GETPWUID_R 1 + +/* Define to 1 if the getrandom() function is available */ +/* #undef HAVE_GETRANDOM */ + +/* Define to 1 if the Linux getrandom() syscall is available */ +#define HAVE_GETRANDOM_SYSCALL 1 + +/* Define to 1 if you have the 'getresgid' function. */ +#define HAVE_GETRESGID 1 + +/* Define to 1 if you have the 'getresuid' function. */ +#define HAVE_GETRESUID 1 + +/* Define to 1 if you have the 'getrusage' function. */ +#define HAVE_GETRUSAGE 1 + +/* Define if you have the 'getservbyname' function. */ +#define HAVE_GETSERVBYNAME 1 + +/* Define if you have the 'getservbyport' function. */ +#define HAVE_GETSERVBYPORT 1 + +/* Define to 1 if you have the 'getsid' function. */ +#define HAVE_GETSID 1 + +/* Define if you have the 'getsockname' function. */ +#define HAVE_GETSOCKNAME 1 + +/* Define to 1 if you have the 'getspent' function. */ +/* #undef HAVE_GETSPENT */ + +/* Define to 1 if you have the 'getspnam' function. */ +/* #undef HAVE_GETSPNAM */ + +/* Define to 1 if you have the 'getuid' function. */ +#define HAVE_GETUID 1 + +/* Define to 1 if you have the 'getwd' function. */ +/* #undef HAVE_GETWD */ + +/* Define if glibc has incorrect _FORTIFY_SOURCE wrappers for memmove and + bcopy. */ +/* #undef HAVE_GLIBC_MEMMOVE_BUG */ + +/* Define to 1 if you have the 'grantpt' function. */ +#define HAVE_GRANTPT 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_GRP_H 1 + +/* Define if you have the 'hstrerror' function. */ +#define HAVE_HSTRERROR 1 + +/* Define this if you have le64toh() */ +#define HAVE_HTOLE64 1 + +/* Define to 1 if you have the 'if_nameindex' function. */ +#define HAVE_IF_NAMEINDEX 1 + +/* Define if you have the 'inet_aton' function. */ +#define HAVE_INET_ATON 1 + +/* Define if you have the 'inet_ntoa' function. */ +#define HAVE_INET_NTOA 1 + +/* Define if you have the 'inet_pton' function. */ +#define HAVE_INET_PTON 1 + +/* Define to 1 if you have the 'initgroups' function. */ +/* #undef HAVE_INITGROUPS */ + +/* Define to 1 if you have the header file. */ +#define HAVE_INTTYPES_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_IO_H */ + +/* Define if gcc has the ipa-pure-const bug. */ +/* #undef HAVE_IPA_PURE_CONST_BUG */ + +/* Define to 1 if you have the 'kill' function. */ +#define HAVE_KILL 1 + +/* Define to 1 if you have the 'killpg' function. */ +#define HAVE_KILLPG 1 + +/* Define if you have the 'kqueue' function. */ +/* #undef HAVE_KQUEUE */ + +/* Define to 1 if you have the header file. */ +#define HAVE_LANGINFO_H 1 + +/* Defined to enable large file support when an off_t is bigger than a long + and long long is at least as big as an off_t. You may need to add some + flags for configuration and compilation to enable this mode. (For Solaris + and Linux, the necessary defines are already defined.) */ +/* #undef HAVE_LARGEFILE_SUPPORT */ + +/* Define to 1 if you have the 'lchflags' function. */ +/* #undef HAVE_LCHFLAGS */ + +/* Define to 1 if you have the 'lchmod' function. */ +/* #undef HAVE_LCHMOD */ + +/* Define to 1 if you have the 'lchown' function. */ +#define HAVE_LCHOWN 1 + +/* Define to 1 if you have the `db' library (-ldb). */ +/* #undef HAVE_LIBDB */ + +/* Define to 1 if you have the 'dl' library (-ldl). */ +#define HAVE_LIBDL 1 + +/* Define to 1 if you have the 'dld' library (-ldld). */ +/* #undef HAVE_LIBDLD */ + +/* Define to 1 if you have the 'ieee' library (-lieee). */ +/* #undef HAVE_LIBIEEE */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_LIBINTL_H */ + +/* Define to 1 if you have the 'sendfile' library (-lsendfile). */ +/* #undef HAVE_LIBSENDFILE */ + +/* Define to 1 if you have the 'sqlite3' library (-lsqlite3). */ +#define HAVE_LIBSQLITE3 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_LIBUTIL_H */ + +/* Define if you have the 'link' function. */ +#define HAVE_LINK 1 + +/* Define to 1 if you have the 'linkat' function. */ +#define HAVE_LINKAT 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINK_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_AUXVEC_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_CAN_BCM_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_CAN_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_CAN_ISOTP_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_CAN_J1939_H 1 + +/* Define if compiling using Linux 3.6 or later. */ +#define HAVE_LINUX_CAN_RAW_FD_FRAMES 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_CAN_RAW_H 1 + +/* Define if compiling using Linux 4.1 or later. */ +#define HAVE_LINUX_CAN_RAW_JOIN_FILTERS 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_FS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_LIMITS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_MEMFD_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_NETFILTER_IPV4_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_NETLINK_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_QRTR_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_RANDOM_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_SCHED_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_SOUNDCARD_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_TIPC_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_VM_SOCKETS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_LINUX_WAIT_H 1 + +/* Define if you have the 'listen' function. */ +#define HAVE_LISTEN 1 + +/* Define to 1 if you have the 'lockf' function. */ +#define HAVE_LOCKF 1 + +/* Define to 1 if you have the 'log1p' function. */ +#define HAVE_LOG1P 1 + +/* Define to 1 if you have the 'log2' function. */ +#define HAVE_LOG2 1 + +/* Define to 1 if you have the `login_tty' function. */ +#define HAVE_LOGIN_TTY 1 + +/* Define to 1 if the system has the type 'long double'. */ +#define HAVE_LONG_DOUBLE 1 + +/* Define to 1 if you have the 'lstat' function. */ +#define HAVE_LSTAT 1 + +/* Define to 1 if you have the 'lutimes' function. */ +/* #undef HAVE_LUTIMES */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_LZMA_H */ + +/* Define to 1 if you have the 'madvise' function. */ +#define HAVE_MADVISE 1 + +/* Define this if you have the makedev macro. */ +#define HAVE_MAKEDEV 1 + +/* Define if you have the 'MAXLOGNAME' constant. */ +/* #undef HAVE_MAXLOGNAME */ + +/* Define to 1 if you have the 'mbrtowc' function. */ +#define HAVE_MBRTOWC 1 + +/* Define if you have the 'memfd_create' function. */ +/* #undef HAVE_MEMFD_CREATE */ + +/* Define to 1 if you have the 'memrchr' function. */ +#define HAVE_MEMRCHR 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_MINIX_CONFIG_H */ + +/* Define to 1 if you have the 'mkdirat' function. */ +#define HAVE_MKDIRAT 1 + +/* Define to 1 if you have the 'mkfifo' function. */ +#define HAVE_MKFIFO 1 + +/* Define to 1 if you have the 'mkfifoat' function. */ +#define HAVE_MKFIFOAT 1 + +/* Define to 1 if you have the 'mknod' function. */ +#define HAVE_MKNOD 1 + +/* Define to 1 if you have the 'mknodat' function. */ +#define HAVE_MKNODAT 1 + +/* Define to 1 if you have the 'mktime' function. */ +#define HAVE_MKTIME 1 + +/* Define to 1 if you have the 'mmap' function. */ +#define HAVE_MMAP 1 + +/* Define to 1 if you have the 'mremap' function. */ +#define HAVE_MREMAP 1 + +/* Define to 1 if you have the 'nanosleep' function. */ +#define HAVE_NANOSLEEP 1 + +/* Define if you have the 'ncurses' library */ +/* #undef HAVE_NCURSES */ + +/* Define if you have the 'ncursesw' library */ +/* #undef HAVE_NCURSESW */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NCURSESW_CURSES_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NCURSESW_NCURSES_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NCURSESW_PANEL_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NCURSES_CURSES_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NCURSES_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NCURSES_NCURSES_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NCURSES_PANEL_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NDBM_H */ + +/* Define to 1 if you have the header file, and it defines 'DIR'. */ +/* #undef HAVE_NDIR_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NETCAN_CAN_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_NETDB_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_NETINET_IN_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_NETLINK_NETLINK_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_NETPACKET_PACKET_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_NET_ETHERNET_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_NET_IF_H 1 + +/* Define to 1 if you have the 'nice' function. */ +#define HAVE_NICE 1 + +/* Define if the internal form of wchar_t in non-Unicode locales is not + Unicode. */ +/* #undef HAVE_NON_UNICODE_WCHAR_T_REPRESENTATION */ + +/* Define to 1 if you have the 'openat' function. */ +#define HAVE_OPENAT 1 + +/* Define to 1 if you have the 'opendir' function. */ +#define HAVE_OPENDIR 1 + +/* Define to 1 if you have the 'openpty' function. */ +#define HAVE_OPENPTY 1 + +/* Define if you have the 'panel' library */ +/* #undef HAVE_PANEL */ + +/* Define if you have the 'panelw' library */ +/* #undef HAVE_PANELW */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_PANEL_H */ + +/* Define to 1 if you have the 'pathconf' function. */ +#define HAVE_PATHCONF 1 + +/* Define to 1 if you have the 'pause' function. */ +#define HAVE_PAUSE 1 + +/* Define to 1 if you have the 'pipe' function. */ +#define HAVE_PIPE 1 + +/* Define to 1 if you have the 'pipe2' function. */ +#define HAVE_PIPE2 1 + +/* Define to 1 if you have the 'plock' function. */ +/* #undef HAVE_PLOCK */ + +/* Define to 1 if you have the 'poll' function. */ +#define HAVE_POLL 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_POLL_H 1 + +/* Define to 1 if you have the 'posix_fadvise' function. */ +#define HAVE_POSIX_FADVISE 1 + +/* Define to 1 if you have the 'posix_fallocate' function. */ +#define HAVE_POSIX_FALLOCATE 1 + +/* Define to 1 if you have the 'posix_openpt' function. */ +#define HAVE_POSIX_OPENPT 1 + +/* Define to 1 if you have the 'posix_spawn' function. */ +/* #undef HAVE_POSIX_SPAWN */ + +/* Define to 1 if you have the 'posix_spawnp' function. */ +/* #undef HAVE_POSIX_SPAWNP */ + +/* Define to 1 if you have the 'posix_spawn_file_actions_addclosefrom_np' + function. */ +/* #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP */ + +/* Define to 1 if you have the 'ppoll' function. */ +#define HAVE_PPOLL 1 + +/* Define to 1 if you have the 'pread' function. */ +#define HAVE_PREAD 1 + +/* Define to 1 if you have the 'preadv' function. */ +#define HAVE_PREADV 1 + +/* Define to 1 if you have the 'preadv2' function. */ +/* #undef HAVE_PREADV2 */ + +/* Define if you have the 'prlimit' function. */ +#define HAVE_PRLIMIT 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_PROCESS_H */ + +/* Define to 1 if you have the 'process_vm_readv' function. */ +#define HAVE_PROCESS_VM_READV 1 + +/* Define if your compiler supports function prototype */ +#define HAVE_PROTOTYPES 1 + +/* Define to 1 if you have the 'pthread_condattr_setclock' function. */ +#define HAVE_PTHREAD_CONDATTR_SETCLOCK 1 + +/* Define to 1 if you have the 'pthread_cond_timedwait_relative_np' function. + */ +/* #undef HAVE_PTHREAD_COND_TIMEDWAIT_RELATIVE_NP */ + +/* Defined for Solaris 2.6 bug in pthread header. */ +/* #undef HAVE_PTHREAD_DESTRUCTOR */ + +/* Define to 1 if you have the 'pthread_getattr_np' function. */ +#define HAVE_PTHREAD_GETATTR_NP 1 + +/* Define to 1 if you have the 'pthread_getcpuclockid' function. */ +#define HAVE_PTHREAD_GETCPUCLOCKID 1 + +/* Define to 1 if you have the 'pthread_getname_np' function. */ +/* #undef HAVE_PTHREAD_GETNAME_NP */ + +/* Define to 1 if you have the 'pthread_get_name_np' function. */ +/* #undef HAVE_PTHREAD_GET_NAME_NP */ + +/* Define to 1 if you have the header file. */ +#define HAVE_PTHREAD_H 1 + +/* Define to 1 if you have the 'pthread_init' function. */ +/* #undef HAVE_PTHREAD_INIT */ + +/* Define to 1 if you have the 'pthread_kill' function. */ +#define HAVE_PTHREAD_KILL 1 + +/* Define to 1 if you have the 'pthread_setname_np' function. */ +#define HAVE_PTHREAD_SETNAME_NP 1 + +/* Define to 1 if you have the 'pthread_set_name_np' function. */ +/* #undef HAVE_PTHREAD_SET_NAME_NP */ + +/* Define to 1 if you have the 'pthread_sigmask' function. */ +#define HAVE_PTHREAD_SIGMASK 1 + +/* Define if platform requires stubbed pthreads support */ +/* #undef HAVE_PTHREAD_STUBS */ + +/* Define to 1 if you have the 'ptsname' function. */ +#define HAVE_PTSNAME 1 + +/* Define to 1 if you have the 'ptsname_r' function. */ +#define HAVE_PTSNAME_R 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_PTY_H 1 + +/* Define to 1 if you have the 'pwrite' function. */ +#define HAVE_PWRITE 1 + +/* Define to 1 if you have the 'pwritev' function. */ +#define HAVE_PWRITEV 1 + +/* Define to 1 if you have the 'pwritev2' function. */ +/* #undef HAVE_PWRITEV2 */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_READLINE_READLINE_H */ + +/* Define to 1 if you have the 'readlink' function. */ +#define HAVE_READLINK 1 + +/* Define to 1 if you have the 'readlinkat' function. */ +#define HAVE_READLINKAT 1 + +/* Define to 1 if you have the 'readv' function. */ +#define HAVE_READV 1 + +/* Define to 1 if you have the 'realpath' function. */ +#define HAVE_REALPATH 1 + +/* Define if you have the 'recvfrom' function. */ +#define HAVE_RECVFROM 1 + +/* Define to 1 if you have the 'renameat' function. */ +#define HAVE_RENAMEAT 1 + +/* Define if readline supports append_history */ +/* #undef HAVE_RL_APPEND_HISTORY */ + +/* Define if you can turn off readline's signal handling. */ +/* #undef HAVE_RL_CATCH_SIGNAL */ + +/* Define to 1 if the system has the type 'rl_compdisp_func_t'. */ +/* #undef HAVE_RL_COMPDISP_FUNC_T */ + +/* Define if you have readline 2.2 */ +/* #undef HAVE_RL_COMPLETION_APPEND_CHARACTER */ + +/* Define if you have readline 4.0 */ +/* #undef HAVE_RL_COMPLETION_DISPLAY_MATCHES_HOOK */ + +/* Define if you have readline 4.2 */ +/* #undef HAVE_RL_COMPLETION_MATCHES */ + +/* Define if you have rl_completion_suppress_append */ +/* #undef HAVE_RL_COMPLETION_SUPPRESS_APPEND */ + +/* Define if you have readline 4.0 */ +/* #undef HAVE_RL_PRE_INPUT_HOOK */ + +/* Define if you have readline 4.0 */ +/* #undef HAVE_RL_RESIZE_TERMINAL */ + +/* Define to 1 if you have the 'rtpSpawn' function. */ +/* #undef HAVE_RTPSPAWN */ + +/* Define to 1 if you have the 'sched_get_priority_max' function. */ +#define HAVE_SCHED_GET_PRIORITY_MAX 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SCHED_H 1 + +/* Define to 1 if you have the 'sched_rr_get_interval' function. */ +#define HAVE_SCHED_RR_GET_INTERVAL 1 + +/* Define to 1 if you have the 'sched_setaffinity' function. */ +#define HAVE_SCHED_SETAFFINITY 1 + +/* Define to 1 if you have the 'sched_setparam' function. */ +#define HAVE_SCHED_SETPARAM 1 + +/* Define to 1 if you have the 'sched_setscheduler' function. */ +#define HAVE_SCHED_SETSCHEDULER 1 + +/* Define to 1 if you have the 'sem_clockwait' function. */ +/* #undef HAVE_SEM_CLOCKWAIT */ + +/* Define to 1 if you have the 'sem_getvalue' function. */ +#define HAVE_SEM_GETVALUE 1 + +/* Define to 1 if you have the 'sem_open' function. */ +/* #undef HAVE_SEM_OPEN */ + +/* Define to 1 if you have the 'sem_timedwait' function. */ +#define HAVE_SEM_TIMEDWAIT 1 + +/* Define to 1 if you have the 'sem_unlink' function. */ +/* #undef HAVE_SEM_UNLINK */ + +/* Define to 1 if you have the 'sendfile' function. */ +#define HAVE_SENDFILE 1 + +/* Define if you have the 'sendto' function. */ +#define HAVE_SENDTO 1 + +/* Define to 1 if you have the 'setegid' function. */ +/* #undef HAVE_SETEGID */ + +/* Define to 1 if you have the 'seteuid' function. */ +/* #undef HAVE_SETEUID */ + +/* Define to 1 if you have the 'setgid' function. */ +/* #undef HAVE_SETGID */ + +/* Define if you have the 'setgroups' function. */ +#define HAVE_SETGROUPS 1 + +/* Define to 1 if you have the 'sethostname' function. */ +/* #undef HAVE_SETHOSTNAME */ + +/* Define to 1 if you have the 'setitimer' function. */ +#define HAVE_SETITIMER 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SETJMP_H 1 + +/* Define to 1 if you have the 'setlocale' function. */ +#define HAVE_SETLOCALE 1 + +/* Define to 1 if you have the 'setns' function. */ +#define HAVE_SETNS 1 + +/* Define to 1 if you have the 'setpgid' function. */ +#define HAVE_SETPGID 1 + +/* Define to 1 if you have the 'setpgrp' function. */ +#define HAVE_SETPGRP 1 + +/* Define to 1 if you have the 'setpriority' function. */ +#define HAVE_SETPRIORITY 1 + +/* Define to 1 if you have the 'setregid' function. */ +/* #undef HAVE_SETREGID */ + +/* Define to 1 if you have the 'setresgid' function. */ +/* #undef HAVE_SETRESGID */ + +/* Define to 1 if you have the 'setresuid' function. */ +/* #undef HAVE_SETRESUID */ + +/* Define to 1 if you have the 'setreuid' function. */ +/* #undef HAVE_SETREUID */ + +/* Define to 1 if you have the 'setsid' function. */ +#define HAVE_SETSID 1 + +/* Define if you have the 'setsockopt' function. */ +#define HAVE_SETSOCKOPT 1 + +/* Define to 1 if you have the 'setuid' function. */ +/* #undef HAVE_SETUID */ + +/* Define to 1 if you have the 'setvbuf' function. */ +#define HAVE_SETVBUF 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SHADOW_H */ + +/* Define to 1 if you have the 'shm_open' function. */ +/* #undef HAVE_SHM_OPEN */ + +/* Define to 1 if you have the 'shm_unlink' function. */ +/* #undef HAVE_SHM_UNLINK */ + +/* Define to 1 if you have the 'shutdown' function. */ +#define HAVE_SHUTDOWN 1 + +/* Define to 1 if you have the 'sigaction' function. */ +#define HAVE_SIGACTION 1 + +/* Define to 1 if you have the 'sigaltstack' function. */ +#define HAVE_SIGALTSTACK 1 + +/* Define to 1 if you have the 'sigfillset' function. */ +#define HAVE_SIGFILLSET 1 + +/* Define to 1 if 'si_band' is a member of 'siginfo_t'. */ +#define HAVE_SIGINFO_T_SI_BAND 1 + +/* Define to 1 if you have the 'siginterrupt' function. */ +#define HAVE_SIGINTERRUPT 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SIGNAL_H 1 + +/* Define to 1 if you have the 'sigpending' function. */ +#define HAVE_SIGPENDING 1 + +/* Define to 1 if you have the 'sigrelse' function. */ +/* #undef HAVE_SIGRELSE */ + +/* Define to 1 if you have the 'sigtimedwait' function. */ +#define HAVE_SIGTIMEDWAIT 1 + +/* Define to 1 if you have the 'sigwait' function. */ +#define HAVE_SIGWAIT 1 + +/* Define to 1 if you have the 'sigwaitinfo' function. */ +#define HAVE_SIGWAITINFO 1 + +/* Define to 1 if you have the 'snprintf' function. */ +#define HAVE_SNPRINTF 1 + +/* struct sockaddr_alg (linux/if_alg.h) */ +#define HAVE_SOCKADDR_ALG 1 + +/* Define if sockaddr has sa_len member */ +/* #undef HAVE_SOCKADDR_SA_LEN */ + +/* struct sockaddr_storage (sys/socket.h) */ +#define HAVE_SOCKADDR_STORAGE 1 + +/* Define if you have the 'socket' function. */ +#define HAVE_SOCKET 1 + +/* Define if you have the 'socketpair' function. */ +#define HAVE_SOCKETPAIR 1 + +/* Define to 1 if the system has the type 'socklen_t'. */ +#define HAVE_SOCKLEN_T 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SPAWN_H 1 + +/* Define to 1 if you have the 'splice' function. */ +#define HAVE_SPLICE 1 + +/* Define to 1 if the system has the type 'ssize_t'. */ +#define HAVE_SSIZE_T 1 + +/* Define to 1 if you have the 'statvfs' function. */ +#define HAVE_STATVFS 1 + +/* Define to 1 if you have the 'statx' function. */ +/* #undef HAVE_STATX */ + +/* Define if you have struct stat.st_mtim.tv_nsec */ +#define HAVE_STAT_TV_NSEC 1 + +/* Define if you have struct stat.st_mtimensec */ +/* #undef HAVE_STAT_TV_NSEC2 */ + +/* Define to 1 if you have the header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDIO_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Has stdatomic.h with atomic_int and atomic_uintptr_t */ +#define HAVE_STD_ATOMIC 1 + +/* Define to 1 if you have the 'strftime' function. */ +#define HAVE_STRFTIME 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRINGS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the 'strlcpy' function. */ +#define HAVE_STRLCPY 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_STROPTS_H */ + +/* Define to 1 if you have the 'strsignal' function. */ +#define HAVE_STRSIGNAL 1 + +/* Define to 1 if 'pw_gecos' is a member of 'struct passwd'. */ +#define HAVE_STRUCT_PASSWD_PW_GECOS 1 + +/* Define to 1 if 'pw_passwd' is a member of 'struct passwd'. */ +#define HAVE_STRUCT_PASSWD_PW_PASSWD 1 + +/* Define to 1 if 'stx_atomic_write_unit_max_opt' is a member of 'struct + statx'. */ +/* #undef HAVE_STRUCT_STATX_STX_ATOMIC_WRITE_UNIT_MAX_OPT */ + +/* Define to 1 if 'stx_atomic_write_unit_min' is a member of 'struct statx'. + */ +/* #undef HAVE_STRUCT_STATX_STX_ATOMIC_WRITE_UNIT_MIN */ + +/* Define to 1 if 'stx_dio_mem_align' is a member of 'struct statx'. */ +/* #undef HAVE_STRUCT_STATX_STX_DIO_MEM_ALIGN */ + +/* Define to 1 if 'stx_dio_read_offset_align' is a member of 'struct statx'. + */ +/* #undef HAVE_STRUCT_STATX_STX_DIO_READ_OFFSET_ALIGN */ + +/* Define to 1 if 'stx_mnt_id' is a member of 'struct statx'. */ +/* #undef HAVE_STRUCT_STATX_STX_MNT_ID */ + +/* Define to 1 if 'stx_subvol' is a member of 'struct statx'. */ +/* #undef HAVE_STRUCT_STATX_STX_SUBVOL */ + +/* Define to 1 if 'st_birthtime' is a member of 'struct stat'. */ +/* #undef HAVE_STRUCT_STAT_ST_BIRTHTIME */ + +/* Define to 1 if 'st_blksize' is a member of 'struct stat'. */ +#define HAVE_STRUCT_STAT_ST_BLKSIZE 1 + +/* Define to 1 if 'st_blocks' is a member of 'struct stat'. */ +#define HAVE_STRUCT_STAT_ST_BLOCKS 1 + +/* Define to 1 if 'st_flags' is a member of 'struct stat'. */ +/* #undef HAVE_STRUCT_STAT_ST_FLAGS */ + +/* Define to 1 if 'st_gen' is a member of 'struct stat'. */ +/* #undef HAVE_STRUCT_STAT_ST_GEN */ + +/* Define to 1 if 'st_rdev' is a member of 'struct stat'. */ +#define HAVE_STRUCT_STAT_ST_RDEV 1 + +/* Define to 1 if 'tm_zone' is a member of 'struct tm'. */ +#define HAVE_STRUCT_TM_TM_ZONE 1 + +/* Define if you have the 'symlink' function. */ +#define HAVE_SYMLINK 1 + +/* Define to 1 if you have the 'symlinkat' function. */ +#define HAVE_SYMLINKAT 1 + +/* Define to 1 if you have the 'sync' function. */ +#define HAVE_SYNC 1 + +/* Define to 1 if you have the 'sysconf' function. */ +#define HAVE_SYSCONF 1 + +/* Define to 1 if you have the 'sysctlbyname' function. */ +/* #undef HAVE_SYSCTLBYNAME */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYSEXITS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYSLOG_H 1 + +/* Define to 1 if you have the 'system' function. */ +#define HAVE_SYSTEM 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_AUDIOIO_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_AUXV_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_BSDTTY_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_DEVPOLL_H */ + +/* Define to 1 if you have the header file, and it defines 'DIR'. + */ +/* #undef HAVE_SYS_DIR_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_ENDIAN_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_EPOLL_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_EVENTFD_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_EVENT_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_FILE_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_IOCTL_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_KERN_CONTROL_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_LOADAVG_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_LOCK_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_MEMFD_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_MKDEV_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_MMAN_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_MODEM_H */ + +/* Define to 1 if you have the header file, and it defines 'DIR'. + */ +/* #undef HAVE_SYS_NDIR_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_PARAM_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_PIDFD_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_POLL_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_RANDOM_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_RESOURCE_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_SELECT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_SENDFILE_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_SOCKET_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_SOUNDCARD_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STATVFS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STAT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_SYSCALL_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_SYSCTL_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_SYSMACROS_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_SYS_DOMAIN_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_TERMIO_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TIMERFD_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TIMES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TIME_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_UIO_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_UN_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_UTSNAME_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_WAIT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_XATTR_H 1 + +/* Define to 1 if you have the 'tcgetpgrp' function. */ +#define HAVE_TCGETPGRP 1 + +/* Define to 1 if you have the 'tcsetpgrp' function. */ +#define HAVE_TCSETPGRP 1 + +/* Define to 1 if you have the 'tempnam' function. */ +#define HAVE_TEMPNAM 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_TERMIOS_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_TERM_H */ + +/* Define to 1 if you have the 'timegm' function. */ +#define HAVE_TIMEGM 1 + +/* Define if you have the 'timerfd_create' function. */ +#define HAVE_TIMERFD_CREATE 1 + +/* Define to 1 if you have the 'times' function. */ +#define HAVE_TIMES 1 + +/* Define to 1 if you have the 'tmpfile' function. */ +#define HAVE_TMPFILE 1 + +/* Define to 1 if you have the 'tmpnam' function. */ +#define HAVE_TMPNAM 1 + +/* Define to 1 if you have the 'tmpnam_r' function. */ +/* #undef HAVE_TMPNAM_R */ + +/* Define to 1 if your 'struct tm' has 'tm_zone'. Deprecated, use + 'HAVE_STRUCT_TM_TM_ZONE' instead. */ +#define HAVE_TM_ZONE 1 + +/* Define to 1 if you have the 'truncate' function. */ +#define HAVE_TRUNCATE 1 + +/* Define to 1 if you have the 'ttyname_r' function. */ +#define HAVE_TTYNAME_R 1 + +/* Define to 1 if you don't have 'tm_zone' but do have the external array + 'tzname'. */ +/* #undef HAVE_TZNAME */ + +/* Define to 1 if you have the 'umask' function. */ +#define HAVE_UMASK 1 + +/* Define to 1 if you have the 'uname' function. */ +#define HAVE_UNAME 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UNISTD_H 1 + +/* Define to 1 if you have the 'unlinkat' function. */ +#define HAVE_UNLINKAT 1 + +/* Define to 1 if you have the 'unlockpt' function. */ +#define HAVE_UNLOCKPT 1 + +/* Define to 1 if you have the 'unshare' function. */ +#define HAVE_UNSHARE 1 + +/* Define if you have a useable wchar_t type defined in wchar.h; useable means + wchar_t must be an unsigned type with at least 16 bits. (see + Include/unicodeobject.h). */ +/* #undef HAVE_USABLE_WCHAR_T */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_UTIL_H */ + +/* Define to 1 if you have the 'utimensat' function. */ +#define HAVE_UTIMENSAT 1 + +/* Define to 1 if you have the 'utimes' function. */ +#define HAVE_UTIMES 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UTIME_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UTMP_H 1 + +/* Define if you have the 'HAVE_UT_NAMESIZE' constant. */ +#define HAVE_UT_NAMESIZE 1 + +/* Define to 1 if you have the 'uuid_create' function. */ +/* #undef HAVE_UUID_CREATE */ + +/* Define to 1 if you have the 'uuid_enc_be' function. */ +/* #undef HAVE_UUID_ENC_BE */ + +/* Define if uuid_generate_time_safe() exists. */ +/* #undef HAVE_UUID_GENERATE_TIME_SAFE */ + +/* Define if uuid_generate_time_safe() is able to deduce a MAC address. */ +/* #undef HAVE_UUID_GENERATE_TIME_SAFE_STABLE_MAC */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_UUID_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_UUID_UUID_H */ + +/* Define to 1 if you have the 'vfork' function. */ +#define HAVE_VFORK 1 + +/* Define to 1 if you have the 'wait' function. */ +#define HAVE_WAIT 1 + +/* Define to 1 if you have the 'wait3' function. */ +/* #undef HAVE_WAIT3 */ + +/* Define to 1 if you have the 'wait4' function. */ +#define HAVE_WAIT4 1 + +/* Define to 1 if you have the 'waitid' function. */ +#define HAVE_WAITID 1 + +/* Define to 1 if you have the 'waitpid' function. */ +#define HAVE_WAITPID 1 + +/* Define if the compiler provides a wchar.h header file. */ +#define HAVE_WCHAR_H 1 + +/* Define to 1 if you have the 'wcscoll' function. */ +#define HAVE_WCSCOLL 1 + +/* Define to 1 if you have the 'wcsftime' function. */ +#define HAVE_WCSFTIME 1 + +/* Define to 1 if you have the 'wcsxfrm' function. */ +#define HAVE_WCSXFRM 1 + +/* Define to 1 if you have the 'wmemcmp' function. */ +#define HAVE_WMEMCMP 1 + +/* Define if tzset() actually switches the local timezone in a meaningful way. + */ +/* #undef HAVE_WORKING_TZSET */ + +/* Define to 1 if you have the 'writev' function. */ +#define HAVE_WRITEV 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_ZDICT_H */ + +/* Define if the zlib library has inflateCopy */ +#define HAVE_ZLIB_COPY 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_ZLIB_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_ZSTD_H */ + +/* Define to 1 if you have the '_getpty' function. */ +/* #undef HAVE__GETPTY */ + +/* Define to 1 if the system has the type '__uint128_t'. */ +#define HAVE___UINT128_T 1 + +/* Define to 1 if 'major', 'minor', and 'makedev' are declared in . + */ +/* #undef MAJOR_IN_MKDEV */ + +/* Define to 1 if 'major', 'minor', and 'makedev' are declared in + . */ +#define MAJOR_IN_SYSMACROS 1 + +/* Define if mvwdelch in curses.h is an expression. */ +/* #undef MVWDELCH_IS_EXPRESSION */ + +/* Define to the address where bug reports for this package should be sent. */ +/* #undef PACKAGE_BUGREPORT */ + +/* Define to the full name of this package. */ +/* #undef PACKAGE_NAME */ + +/* Define to the full name and version of this package. */ +/* #undef PACKAGE_STRING */ + +/* Define to the one symbol short name of this package. */ +/* #undef PACKAGE_TARNAME */ + +/* Define to the home page for this package. */ +/* #undef PACKAGE_URL */ + +/* Define to the version of this package. */ +/* #undef PACKAGE_VERSION */ + +/* Define if POSIX semaphores aren't enabled on your system */ +/* #undef POSIX_SEMAPHORES_NOT_ENABLED */ + +/* Define if pthread_key_t is compatible with int. */ +#define PTHREAD_KEY_T_IS_COMPATIBLE_WITH_INT 1 + +/* Defined if PTHREAD_SCOPE_SYSTEM supported. */ +/* #undef PTHREAD_SYSTEM_SCHED_SUPPORTED */ + +/* Define as the preferred size in bits of long digits */ +/* #undef PYLONG_BITS_IN_DIGIT */ + +/* Define to use huge pages for pymalloc arenas */ +/* #undef PYMALLOC_USE_HUGEPAGES */ + +/* enabled builtin hash modules */ +#define PY_BUILTIN_HASHLIB_HASHES "md5,sha1,sha2,sha3,blake2" + +/* Define if you want to coerce the C locale to a UTF-8 based locale */ +#define PY_COERCE_C_LOCALE 1 + +/* Define to 1 if you have the perf trampoline. */ +/* #undef PY_HAVE_PERF_TRAMPOLINE */ + +/* Define to 1 to build the sqlite module with loadable extensions support. */ +/* #undef PY_SQLITE_ENABLE_LOAD_EXTENSION */ + +/* Define if SQLite was compiled with the serialize API */ +#define PY_SQLITE_HAVE_SERIALIZE 1 + +/* Default cipher suites list for ssl module. 1: Python's preferred selection, + 2: leave OpenSSL defaults untouched, 0: custom string */ +#define PY_SSL_DEFAULT_CIPHERS 1 + +/* Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0 */ +/* #undef PY_SSL_DEFAULT_CIPHER_STRING */ + +/* PEP 11 Support tier (1, 2, 3 or 0 for unsupported) */ +#define PY_SUPPORT_TIER 3 + +/* Define if you want to build an interpreter with many run-time checks. */ +/* #undef Py_DEBUG */ + +/* Defined if Python is built as a shared library. */ +#define Py_ENABLE_SHARED 1 + +/* Define if you want to disable the GIL */ +/* #undef Py_GIL_DISABLED */ + +/* Define hash algorithm for str, bytes and memoryview. SipHash24: 1, FNV: 2, + SipHash13: 3, externally defined: 0 */ +/* #undef Py_HASH_ALGORITHM */ + +/* Define if you want to enable remote debugging support. */ +#define Py_REMOTE_DEBUG 1 + +/* Define if rl_startup_hook takes arguments */ +/* #undef Py_RL_STARTUP_HOOK_TAKES_ARGS */ + +/* Define if you want to enable internal statistics gathering. */ +/* #undef Py_STATS */ + +/* The version of SunOS/Solaris as reported by `uname -r' without the dot. */ +/* #undef Py_SUNOS_VERSION */ + +/* Define if you want to enable tracing references for debugging purpose */ +/* #undef Py_TRACE_REFS */ + +/* assume C89 semantics that RETSIGTYPE is always void */ +#define RETSIGTYPE void + +/* Define if setpgrp() must be called as setpgrp(0, 0). */ +/* #undef SETPGRP_HAVE_ARG */ + +/* Define if i>>j for signed int i does not extend the sign bit when i < 0 */ +/* #undef SIGNED_RIGHT_SHIFT_ZERO_FILLS */ + +/* The size of 'double', as computed by sizeof. */ +#define SIZEOF_DOUBLE 8 + +/* The size of 'float', as computed by sizeof. */ +#define SIZEOF_FLOAT 4 + +/* The size of 'fpos_t', as computed by sizeof. */ +#define SIZEOF_FPOS_T 8 + +/* The size of 'int', as computed by sizeof. */ +#define SIZEOF_INT 4 + +/* The size of 'long', as computed by sizeof. */ +#define SIZEOF_LONG 8 + +/* The size of 'long double', as computed by sizeof. */ +#define SIZEOF_LONG_DOUBLE 16 + +/* The size of 'long long', as computed by sizeof. */ +#define SIZEOF_LONG_LONG 8 + +/* The size of 'off_t', as computed by sizeof. */ +#define SIZEOF_OFF_T 8 + +/* The size of 'pid_t', as computed by sizeof. */ +#define SIZEOF_PID_T 4 + +/* The size of 'pthread_key_t', as computed by sizeof. */ +#define SIZEOF_PTHREAD_KEY_T 4 + +/* The size of 'pthread_t', as computed by sizeof. */ +#define SIZEOF_PTHREAD_T 8 + +/* The size of 'short', as computed by sizeof. */ +#define SIZEOF_SHORT 2 + +/* The size of 'size_t', as computed by sizeof. */ +#define SIZEOF_SIZE_T 8 + +/* The size of 'time_t', as computed by sizeof. */ +#define SIZEOF_TIME_T 8 + +/* The size of 'uintptr_t', as computed by sizeof. */ +#define SIZEOF_UINTPTR_T 8 + +/* The size of 'void *', as computed by sizeof. */ +#define SIZEOF_VOID_P 8 + +/* The size of 'wchar_t', as computed by sizeof. */ +#define SIZEOF_WCHAR_T 4 + +/* The size of '_Bool', as computed by sizeof. */ +#define SIZEOF__BOOL 1 + +/* Platform tag, used in binary module extension filenames. */ +#define SOABI_PLATFORM "aarch64-linux-android" + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* Define if you can safely include both and + (which you can't on SCO ODT 3.0). */ +#define SYS_SELECT_WITH_SYS_TIME 1 + +/* Custom thread stack size depending on chosen sanitizer runtimes. */ +/* #undef THREAD_STACK_SIZE */ + +/* Library needed by timemodule.c: librt may be needed for clock_gettime() */ +/* #undef TIMEMODULE_LIB */ + +/* Define to 1 if your declares 'struct tm'. */ +/* #undef TM_IN_SYS_TIME */ + +/* Define if you want to use computed gotos in ceval.c. */ +/* #undef USE_COMPUTED_GOTOS */ + +/* Enable extensions on AIX, Interix, z/OS. */ +#ifndef _ALL_SOURCE +# define _ALL_SOURCE 1 +#endif +/* Enable general extensions on macOS. */ +#ifndef _DARWIN_C_SOURCE +# define _DARWIN_C_SOURCE 1 +#endif +/* Enable general extensions on Solaris. */ +#ifndef __EXTENSIONS__ +# define __EXTENSIONS__ 1 +#endif +/* Enable GNU extensions on systems that have them. */ +#ifndef _GNU_SOURCE +# define _GNU_SOURCE 1 +#endif +/* Enable X/Open compliant socket functions that do not require linking + with -lxnet on HP-UX 11.11. */ +#ifndef _HPUX_ALT_XOPEN_SOCKET_API +# define _HPUX_ALT_XOPEN_SOCKET_API 1 +#endif +/* Identify the host operating system as Minix. + This macro does not affect the system headers' behavior. + A future release of Autoconf may stop defining this macro. */ +#ifndef _MINIX +/* # undef _MINIX */ +#endif +/* Enable general extensions on NetBSD. + Enable NetBSD compatibility extensions on Minix. */ +#ifndef _NETBSD_SOURCE +# define _NETBSD_SOURCE 1 +#endif +/* Enable OpenBSD compatibility extensions on NetBSD. + Oddly enough, this does nothing on OpenBSD. */ +#ifndef _OPENBSD_SOURCE +# define _OPENBSD_SOURCE 1 +#endif +/* Define to 1 if needed for POSIX-compatible behavior. */ +#ifndef _POSIX_SOURCE +/* # undef _POSIX_SOURCE */ +#endif +/* Define to 2 if needed for POSIX-compatible behavior. */ +#ifndef _POSIX_1_SOURCE +/* # undef _POSIX_1_SOURCE */ +#endif +/* Enable POSIX-compatible threading on Solaris. */ +#ifndef _POSIX_PTHREAD_SEMANTICS +# define _POSIX_PTHREAD_SEMANTICS 1 +#endif +/* Enable extensions specified by ISO/IEC TS 18661-5:2014. */ +#ifndef __STDC_WANT_IEC_60559_ATTRIBS_EXT__ +# define __STDC_WANT_IEC_60559_ATTRIBS_EXT__ 1 #endif +/* Enable extensions specified by ISO/IEC TS 18661-1:2014. */ +#ifndef __STDC_WANT_IEC_60559_BFP_EXT__ +# define __STDC_WANT_IEC_60559_BFP_EXT__ 1 +#endif +/* Enable extensions specified by ISO/IEC TS 18661-2:2015. */ +#ifndef __STDC_WANT_IEC_60559_DFP_EXT__ +# define __STDC_WANT_IEC_60559_DFP_EXT__ 1 +#endif +/* Enable extensions specified by C23 Annex F. */ +#ifndef __STDC_WANT_IEC_60559_EXT__ +# define __STDC_WANT_IEC_60559_EXT__ 1 +#endif +/* Enable extensions specified by ISO/IEC TS 18661-4:2015. */ +#ifndef __STDC_WANT_IEC_60559_FUNCS_EXT__ +# define __STDC_WANT_IEC_60559_FUNCS_EXT__ 1 +#endif +/* Enable extensions specified by C23 Annex H and ISO/IEC TS 18661-3:2015. */ +#ifndef __STDC_WANT_IEC_60559_TYPES_EXT__ +# define __STDC_WANT_IEC_60559_TYPES_EXT__ 1 +#endif +/* Enable extensions specified by ISO/IEC TR 24731-2:2010. */ +#ifndef __STDC_WANT_LIB_EXT2__ +# define __STDC_WANT_LIB_EXT2__ 1 +#endif +/* Enable extensions specified by ISO/IEC 24747:2009. */ +#ifndef __STDC_WANT_MATH_SPEC_FUNCS__ +# define __STDC_WANT_MATH_SPEC_FUNCS__ 1 +#endif +/* Enable extensions on HP NonStop. */ +#ifndef _TANDEM_SOURCE +# define _TANDEM_SOURCE 1 +#endif +/* Enable X/Open extensions. Define to 500 only if necessary + to make mbstate_t available. */ +#ifndef _XOPEN_SOURCE +# define _XOPEN_SOURCE 800 +#endif + + +/* Define if WINDOW in curses.h offers a field _flags. */ +/* #undef WINDOW_HAS_FLAGS */ + +/* Define if you want build the _decimal module using a coroutine-local rather + than a thread-local context */ +#define WITH_DECIMAL_CONTEXTVAR 1 + +/* Define if you want documentation strings in extension modules */ +#define WITH_DOC_STRINGS 1 + +/* Define if you want to compile in DTrace support */ +/* #undef WITH_DTRACE */ + +/* Define if you want to use the new-style (Openstep, Rhapsody, MacOS) dynamic + linker (dyld) instead of the old-style (NextStep) dynamic linker (rld). + Dyld is necessary to support frameworks. */ +/* #undef WITH_DYLD */ + +/* Define to build the readline module against libedit. */ +/* #undef WITH_EDITLINE */ + +/* Define to 1 if libintl is needed for locale functions. */ +/* #undef WITH_LIBINTL */ + +/* Define if you want to compile in mimalloc memory allocator. */ +#define WITH_MIMALLOC 1 + +/* Define if you want to produce an OpenStep/Rhapsody framework (shared + library plus accessory files). */ +/* #undef WITH_NEXT_FRAMEWORK */ + +/* Define if you want to compile in Python-specific mallocs */ +#define WITH_PYMALLOC 1 + +/* Define if you want pymalloc to be disabled when running under valgrind */ +/* #undef WITH_VALGRIND */ + +/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most + significant byte first (like Motorola and SPARC, unlike Intel). */ +#if defined AC_APPLE_UNIVERSAL_BUILD +# if defined __BIG_ENDIAN__ +# define WORDS_BIGENDIAN 1 +# endif +#else +# ifndef WORDS_BIGENDIAN +/* # undef WORDS_BIGENDIAN */ +# endif +#endif + +/* Define if arithmetic is subject to x87-style double rounding issue */ +/* #undef X87_DOUBLE_ROUNDING */ + +/* Define on OpenBSD to activate all library features */ +/* #undef _BSD_SOURCE */ + +/* Define on Darwin to activate all library features */ +#define _DARWIN_C_SOURCE 1 + +/* This must be set to 64 on some systems to enable large file support. */ +#define _FILE_OFFSET_BITS 64 + +/* Define to include mbstate_t for mbrtowc */ +/* #undef _INCLUDE__STDC_A1_SOURCE */ + +/* This must be defined on some systems to enable large file support. */ +#define _LARGEFILE_SOURCE 1 + +/* This must be defined on AIX systems to enable large file support. */ +/* #undef _LARGE_FILES */ + +/* Define on NetBSD to activate all library features */ +#define _NETBSD_SOURCE 1 + +/* Define to activate features from IEEE Std 1003.1-2024 */ +#define _POSIX_C_SOURCE 202405L -#ifdef __ANDROID__ - #ifdef __aarch64__ - #include "pyconfig-android-aarch64.h" - #endif +/* Define if you have POSIX threads, and your system does not define that. */ +/* #undef _POSIX_THREADS */ - #ifdef __x86_64__ - #include "pyconfig-android-x86_64.h" - #endif +/* framework name */ +#define _PYTHONFRAMEWORK "" + +/* Maximum length in bytes of a thread name */ +#define _PYTHREAD_NAME_MAXLEN 15 + +/* Defined if _Complex C type can be used with libffi. */ +/* #undef _Py_FFI_SUPPORT_C_COMPLEX */ + +/* HACL* library can compile SIMD128 implementations */ +/* #undef _Py_HACL_CAN_COMPILE_VEC128 */ + +/* HACL* library can compile SIMD256 implementations */ +/* #undef _Py_HACL_CAN_COMPILE_VEC256 */ + +/* Define if compiler supports __builtin_shufflevector with 128-bit vectors + AND the target architecture has native SIMD (not just API availability) */ +#define _Py_HAVE_EFFICIENT_BUILTIN_SHUFFLEVECTOR 1 + +/* Define to 1 if libgcc __register_frame and __deregister_frame are linkable. + */ +#define _Py_HAVE_LIBGCC_EH_FRAME_REGISTRATION 1 + +/* Define if you have the 'PR_SET_VMA_ANON_NAME' constant. */ +#define _Py_HAVE_PR_SET_VMA_ANON_NAME 1 + +/* Thread stack size set by the linker (in bytes). */ +/* #undef _Py_LINKER_THREAD_STACK_SIZE */ + +/* Define to 1 if the machine stack grows down (default); 0 if it grows up. */ +#define _Py_STACK_GROWS_DOWN 1 + +/* Define if you want to use tail-calling interpreters in CPython. */ +/* #undef _Py_TAIL_CALL_INTERP */ + +/* Define to 1 if frame unwinding via pointers is expected to work, 0 if not. + Leave undefined if unknown. */ +#define _Py_WITH_FRAME_POINTERS 1 + +/* Define to force use of thread-safe errno, h_errno, and other functions */ +#define _REENTRANT 1 + +/* Define to 1 if you want to emulate getpid() on WASI */ +/* #undef _WASI_EMULATED_GETPID */ + +/* Define to 1 if you want to emulate process clocks on WASI */ +/* #undef _WASI_EMULATED_PROCESS_CLOCKS */ + +/* Define to 1 if you want to emulate signals on WASI */ +/* #undef _WASI_EMULATED_SIGNAL */ + +/* Define to the level of X/Open that your system supports */ +#define _XOPEN_SOURCE 800 + +/* Define to activate Unix95-and-earlier features */ +#define _XOPEN_SOURCE_EXTENDED 1 + +/* Define on FreeBSD to activate all library features */ +#define __BSD_VISIBLE 1 + +/* Define to 'long' if does not define clock_t. */ +/* #undef clock_t */ + +/* Define to empty if 'const' does not conform to ANSI C. */ +/* #undef const */ + +/* Define as 'int' if doesn't define. */ +/* #undef gid_t */ + +/* Define to 'int' if does not define. */ +/* #undef mode_t */ + +/* Define to 'long int' if does not define. */ +/* #undef off_t */ + +/* Define as a signed integer type capable of holding a process identifier. */ +/* #undef pid_t */ + +/* Define to empty if the keyword does not work. */ +/* #undef signed */ + +/* Define as 'unsigned int' if doesn't define. */ +/* #undef size_t */ + +/* Define to 'int' if does not define. */ +/* #undef socklen_t */ + +/* Define as 'int' if doesn't define. */ +/* #undef uid_t */ + + +/* Define the macros needed if on a UnixWare 7.x system. */ +#if defined(__USLC__) && defined(__SCO_VERSION__) +#define STRICT_SYSV_CURSES /* Don't use ncurses extensions */ #endif + +#endif /*Py_PYCONFIG_H*/ + diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyerrors.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyerrors.h index 5d0028c11..cfabbc5fe 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pyerrors.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pyerrors.h @@ -91,6 +91,9 @@ PyAPI_DATA(PyObject *) PyExc_EOFError; PyAPI_DATA(PyObject *) PyExc_FloatingPointError; PyAPI_DATA(PyObject *) PyExc_OSError; PyAPI_DATA(PyObject *) PyExc_ImportError; +#if !defined(Py_LIMITED_API) +PyAPI_DATA(PyObject *) PyExc_ImportCycleError; +#endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03060000 PyAPI_DATA(PyObject *) PyExc_ModuleNotFoundError; #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pylifecycle.h b/python-multiplatform/src/nativeInterop/cinterop/include/pylifecycle.h index de1bcb1d2..4b3474035 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pylifecycle.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pylifecycle.h @@ -35,15 +35,8 @@ PyAPI_FUNC(int) Py_BytesMain(int argc, char **argv); /* In pathconfig.c */ Py_DEPRECATED(3.11) PyAPI_FUNC(void) Py_SetProgramName(const wchar_t *); -Py_DEPRECATED(3.13) PyAPI_FUNC(wchar_t *) Py_GetProgramName(void); - Py_DEPRECATED(3.11) PyAPI_FUNC(void) Py_SetPythonHome(const wchar_t *); -Py_DEPRECATED(3.13) PyAPI_FUNC(wchar_t *) Py_GetPythonHome(void); -Py_DEPRECATED(3.13) PyAPI_FUNC(wchar_t *) Py_GetProgramFullPath(void); -Py_DEPRECATED(3.13) PyAPI_FUNC(wchar_t *) Py_GetPrefix(void); -Py_DEPRECATED(3.13) PyAPI_FUNC(wchar_t *) Py_GetExecPrefix(void); -Py_DEPRECATED(3.13) PyAPI_FUNC(wchar_t *) Py_GetPath(void); #ifdef MS_WINDOWS int _Py_CheckPython3(void); #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pymacconfig.h b/python-multiplatform/src/nativeInterop/cinterop/include/pymacconfig.h index 615abe103..9d63ddf8a 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pymacconfig.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pymacconfig.h @@ -18,7 +18,6 @@ #undef SIZEOF_UINTPTR_T #undef SIZEOF_PTHREAD_T #undef WORDS_BIGENDIAN -#undef DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754 #undef DOUBLE_IS_BIG_ENDIAN_IEEE754 #undef DOUBLE_IS_LITTLE_ENDIAN_IEEE754 #undef HAVE_GCC_ASM_FOR_X87 diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pymacro.h b/python-multiplatform/src/nativeInterop/cinterop/include/pymacro.h index 1af057ad7..7ecce44a0 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pymacro.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pymacro.h @@ -24,46 +24,90 @@ #endif -// _Py_ALIGN_AS: this compiler's spelling of `alignas` keyword, -// We currently use alignas for free-threaded builds only; additional compat -// checking would be great before we add it to the default build. -// Standards/compiler support: +// _Py_ALIGNED_DEF(N, T): Define a variable/member with increased alignment +// +// `N`: the desired minimum alignment, an integer literal, number of bytes +// `T`: the type of the defined variable +// (or a type with at least the defined variable's alignment) +// +// May not be used on a struct definition. +// +// Standards/compiler support for `alignas` alternatives: // - `alignas` is a keyword in C23 and C++11. // - `_Alignas` is a keyword in C11 // - GCC & clang has __attribute__((aligned)) // (use that for older standards in pedantic mode) // - MSVC has __declspec(align) // - `_Alignas` is common C compiler extension -// Older compilers may name it differently; to allow compilation on such -// unsupported platforms, we don't redefine _Py_ALIGN_AS if it's already +// Older compilers may name `alignas` differently; to allow compilation on such +// unsupported platforms, we don't redefine _Py_ALIGNED_DEF if it's already // defined. Note that defining it wrong (including defining it to nothing) will // cause ABI incompatibilities. -#ifdef Py_GIL_DISABLED -# ifndef _Py_ALIGN_AS -# ifdef __cplusplus -# if __cplusplus >= 201103L -# define _Py_ALIGN_AS(V) alignas(V) -# elif defined(__GNUC__) || defined(__clang__) -# define _Py_ALIGN_AS(V) __attribute__((aligned(V))) -# elif defined(_MSC_VER) -# define _Py_ALIGN_AS(V) __declspec(align(V)) -# else -# define _Py_ALIGN_AS(V) alignas(V) -# endif -# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L -# define _Py_ALIGN_AS(V) alignas(V) -# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L -# define _Py_ALIGN_AS(V) _Alignas(V) -# elif (defined(__GNUC__) || defined(__clang__)) -# define _Py_ALIGN_AS(V) __attribute__((aligned(V))) -# elif defined(_MSC_VER) -# define _Py_ALIGN_AS(V) __declspec(align(V)) -# else -# define _Py_ALIGN_AS(V) _Alignas(V) -# endif +// +// Behavior of `alignas` alternatives: +// - `alignas` & `_Alignas`: +// - Can be used multiple times; the greatest alignment applies. +// - It is an *error* if the combined effect of all `alignas` modifiers would +// decrease the alignment. +// - Takes types or numbers. +// - May not be used on a struct definition, unless also defining a variable. +// - `__declspec(align)`: +// - Has no effect if it would decrease alignment. +// - Only takes an integer literal. +// - May be used on struct or variable definitions. +// However, when defining both the struct and the variable at once, +// `declspec(aligned)` causes compiler warning 5274 and possible ABI +// incompatibility. +// - ` __attribute__((aligned))`: +// - Has no effect if it would decrease alignment. +// - Takes types or numbers +// - May be used on struct or variable definitions. +#ifndef _Py_ALIGNED_DEF +# ifdef __cplusplus +# if __cplusplus >= 201103L +# define _Py_ALIGNED_DEF(N, T) alignas(N) alignas(T) T +# elif defined(__GNUC__) || defined(__clang__) +# define _Py_ALIGNED_DEF(N, T) __attribute__((aligned(N))) T +# elif defined(_MSC_VER) +# define _Py_ALIGNED_DEF(N, T) __declspec(align(N)) T +# else +# define _Py_ALIGNED_DEF(N, T) alignas(N) alignas(T) T +# endif +# elif defined(_MSC_VER) +# define _Py_ALIGNED_DEF(N, T) __declspec(align(N)) T +# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L +# define _Py_ALIGNED_DEF(N, T) alignas(N) alignas(T) T +# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define _Py_ALIGNED_DEF(N, T) _Alignas(N) _Alignas(T) T +# elif (defined(__GNUC__) || defined(__clang__)) +# define _Py_ALIGNED_DEF(N, T) __attribute__((aligned(N))) T +# else +# define _Py_ALIGNED_DEF(N, T) _Alignas(N) _Alignas(T) T +# endif +#endif + + +// _Py_ANONYMOUS: modifier for declaring an anonymous union. +// Usage: _Py_ANONYMOUS union { ... }; +// Standards/compiler support: +// - C++ allows anonymous unions, but not structs +// - C11 and above allows anonymous unions and structs +// - MSVC has warning(disable: 4201) "nonstandard extension used : nameless +// struct/union". This is specific enough that we disable it for all of +// Python.h. +// - GCC & clang needs __extension__ before C11 +// To allow unsupported platforms which need other spellings, we use a +// predefined value of _Py_ANONYMOUS if it exists. +#ifndef _Py_ANONYMOUS +# if (defined(__GNUC__) || defined(__clang__)) \ + && !(defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L) +# define _Py_ANONYMOUS __extension__ +# else +# define _Py_ANONYMOUS # endif #endif + /* Minimum value between x and y */ #define Py_MIN(x, y) (((x) > (y)) ? (y) : (x)) @@ -237,12 +281,13 @@ // "comparison of unsigned expression in '< 0' is always false". #define _Py_IS_TYPE_SIGNED(type) ((type)(-1) <= 0) -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000 // 3.14 // Version helpers. These are primarily macros, but have exported equivalents. +#define _Py_PACK_VERSION(X, Y) _Py_PACK_FULL_VERSION(X, Y, 0, 0, 0) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 14) PyAPI_FUNC(uint32_t) Py_PACK_FULL_VERSION(int x, int y, int z, int level, int serial); PyAPI_FUNC(uint32_t) Py_PACK_VERSION(int x, int y); #define Py_PACK_FULL_VERSION _Py_PACK_FULL_VERSION -#define Py_PACK_VERSION(X, Y) Py_PACK_FULL_VERSION(X, Y, 0, 0, 0) +#define Py_PACK_VERSION _Py_PACK_VERSION #endif // Py_LIMITED_API < 3.14 diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pymath.h b/python-multiplatform/src/nativeInterop/cinterop/include/pymath.h index 0ead1f956..7cfe44136 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pymath.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pymath.h @@ -7,6 +7,7 @@ /* High precision definition of pi and e (Euler) * The values are taken from libc6's math.h. */ +// Deprecated since Python 3.15. #ifndef Py_MATH_PIl #define Py_MATH_PIl 3.1415926535897932384626433832795029L #endif @@ -14,6 +15,7 @@ #define Py_MATH_PI 3.14159265358979323846 #endif +// Deprecated since Python 3.15. #ifndef Py_MATH_El #define Py_MATH_El 2.7182818284590452353602874713526625L #endif @@ -43,13 +45,14 @@ #define Py_IS_FINITE(X) isfinite(X) // Py_INFINITY: Value that evaluates to a positive double infinity. +// Soft deprecated since Python 3.15, use INFINITY instead. #ifndef Py_INFINITY # define Py_INFINITY ((double)INFINITY) #endif /* Py_HUGE_VAL should always be the same as Py_INFINITY. But historically * this was not reliable and Python did not require IEEE floats and C99 - * conformity. The macro was soft deprecated in Python 3.14, use Py_INFINITY instead. + * conformity. The macro was soft deprecated in Python 3.14, use INFINITY instead. */ #ifndef Py_HUGE_VAL # define Py_HUGE_VAL HUGE_VAL @@ -57,9 +60,24 @@ /* Py_NAN: Value that evaluates to a quiet Not-a-Number (NaN). The sign is * undefined and normally not relevant, but e.g. fixed for float("nan"). + * + * Note: On Solaris, NAN is a function address, hence arithmetic is impossible. + * For that reason, we instead use the built-in call if available or fallback + * to a generic NaN computed from strtod() as a last resort. + * + * See https://github.com/python/cpython/issues/136006 for details. */ #if !defined(Py_NAN) -# define Py_NAN ((double)NAN) +# if defined(__sun) +# if _Py__has_builtin(__builtin_nanf) +# define Py_NAN ((double)__builtin_nanf("")) +# else +# include +# define Py_NAN (strtod("NAN", NULL)) +# endif +# else +# define Py_NAN ((double)NAN) +# endif #endif #endif /* Py_PYMATH_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pyport.h b/python-multiplatform/src/nativeInterop/cinterop/include/pyport.h index 2c8567f25..73a3e6cda 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pyport.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pyport.h @@ -58,26 +58,6 @@ #endif -/* Defines to build Python and its standard library: - * - * - Py_BUILD_CORE: Build Python core. Give access to Python internals, but - * should not be used by third-party modules. - * - Py_BUILD_CORE_BUILTIN: Build a Python stdlib module as a built-in module. - * - Py_BUILD_CORE_MODULE: Build a Python stdlib module as a dynamic library. - * - * Py_BUILD_CORE_BUILTIN and Py_BUILD_CORE_MODULE imply Py_BUILD_CORE. - * - * On Windows, Py_BUILD_CORE_MODULE exports "PyInit_xxx" symbol, whereas - * Py_BUILD_CORE_BUILTIN does not. - */ -#if defined(Py_BUILD_CORE_BUILTIN) && !defined(Py_BUILD_CORE) -# define Py_BUILD_CORE -#endif -#if defined(Py_BUILD_CORE_MODULE) && !defined(Py_BUILD_CORE) -# define Py_BUILD_CORE -#endif - - /************************************************************************** Symbols and macros to supply platform-independent interfaces to basic C language & library operations whose spellings vary across platforms. @@ -385,17 +365,6 @@ extern "C" { # define Py_NO_INLINE #endif -#include "exports.h" - -#ifdef Py_LIMITED_API - // The internal C API must not be used with the limited C API: make sure - // that Py_BUILD_CORE macro is not defined in this case. These 3 macros are - // used by exports.h, so only undefine them afterwards. -# undef Py_BUILD_CORE -# undef Py_BUILD_CORE_BUILTIN -# undef Py_BUILD_CORE_MODULE -#endif - /* limits.h constants that may be missing */ #ifndef INT_MAX @@ -446,7 +415,9 @@ extern "C" { /* * Specify alignment on compilers that support it. */ -#if defined(__GNUC__) && __GNUC__ >= 3 +#ifdef Py_BUILD_CORE +// always use _Py_ALIGNED_DEF instead +#elif defined(__GNUC__) && __GNUC__ >= 3 #define Py_ALIGNED(x) __attribute__((aligned(x))) #else #define Py_ALIGNED(x) @@ -504,28 +475,30 @@ extern "C" { * Thread support is stubbed and any attempt to create a new thread fails. */ #if (!defined(HAVE_PTHREAD_STUBS) && \ + !defined(__wasi__) && \ (!defined(__EMSCRIPTEN__) || defined(__EMSCRIPTEN_PTHREADS__))) # define Py_CAN_START_THREADS 1 #endif + +/* gh-142163: Some libraries rely on HAVE_THREAD_LOCAL being undefined, so + * we can only define it only when Py_BUILD_CORE is set.*/ +#ifdef Py_BUILD_CORE +// This is no longer coupled to _Py_thread_local. +# define HAVE_THREAD_LOCAL 1 +#endif + #ifdef WITH_THREAD -# ifdef Py_BUILD_CORE -# ifdef HAVE_THREAD_LOCAL -# error "HAVE_THREAD_LOCAL is already defined" -# endif -# define HAVE_THREAD_LOCAL 1 -# ifdef thread_local -# define _Py_thread_local thread_local -# elif __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) -# define _Py_thread_local _Thread_local -# elif defined(_MSC_VER) /* AKA NT_THREADS */ -# define _Py_thread_local __declspec(thread) -# elif defined(__GNUC__) /* includes clang */ -# define _Py_thread_local __thread -# else - // fall back to the PyThread_tss_*() API, or ignore. -# undef HAVE_THREAD_LOCAL -# endif +# ifdef thread_local +# define _Py_thread_local thread_local +# elif __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) +# define _Py_thread_local _Thread_local +# elif defined(_MSC_VER) /* AKA NT_THREADS */ +# define _Py_thread_local __declspec(thread) +# elif defined(__GNUC__) /* includes clang */ +# define _Py_thread_local __thread +# else +# error "no supported thread-local variable storage classifier" # endif #endif @@ -565,8 +538,11 @@ extern "C" { // // Example: _Py_TYPEOF(x) x_copy = (x); // -// The macro is only defined if GCC or clang compiler is used. -#if defined(__GNUC__) || defined(__clang__) +// On C23, use typeof(). Otherwise, the macro is only defined +// if GCC or clang compiler is used. +#if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 202311L +# define _Py_TYPEOF(expr) typeof(expr) +#elif defined(__GNUC__) || defined(__clang__) # define _Py_TYPEOF(expr) __typeof__(expr) #endif @@ -577,6 +553,7 @@ extern "C" { # if !defined(_Py_MEMORY_SANITIZER) # define _Py_MEMORY_SANITIZER # define _Py_NO_SANITIZE_MEMORY __attribute__((no_sanitize_memory)) +# define _Py_MSAN_UNPOISON(PTR, SIZE) (__msan_unpoison(PTR, SIZE)) # endif # endif # if __has_feature(address_sanitizer) @@ -615,6 +592,9 @@ extern "C" { #ifndef _Py_NO_SANITIZE_MEMORY # define _Py_NO_SANITIZE_MEMORY #endif +#ifndef _Py_MSAN_UNPOISON +# define _Py_MSAN_UNPOISON(PTR, SIZE) +#endif /* AIX has __bool__ redefined in it's system header file. */ #if defined(_AIX) && defined(__bool__) @@ -667,25 +647,6 @@ extern "C" { #endif -// _Py_NO_SANITIZE_UNDEFINED(): Disable Undefined Behavior sanitizer (UBsan) -// on a function. -// -// Clang and GCC 9.0+ use __attribute__((no_sanitize("undefined"))). -// GCC 4.9+ uses __attribute__((no_sanitize_undefined)). -#if defined(__has_feature) -# if __has_feature(undefined_behavior_sanitizer) -# define _Py_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize("undefined"))) -# endif -#endif -#if !defined(_Py_NO_SANITIZE_UNDEFINED) && defined(__GNUC__) \ - && ((__GNUC__ >= 5) || (__GNUC__ == 4) && (__GNUC_MINOR__ >= 9)) -# define _Py_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize_undefined)) -#endif -#ifndef _Py_NO_SANITIZE_UNDEFINED -# define _Py_NO_SANITIZE_UNDEFINED -#endif - - // _Py_NONSTRING: The nonstring variable attribute specifies that an object or // member declaration with type array of char, signed char, or unsigned char, // or pointer to such a type is intended to store character arrays that do not diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pystate.h b/python-multiplatform/src/nativeInterop/cinterop/include/pystate.h index 727b8fbff..8dad74823 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pystate.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pystate.h @@ -120,6 +120,29 @@ PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE); PyAPI_FUNC(PyThreadState *) PyGILState_GetThisThreadState(void); +/* PEP 788 -- Protection against interpreter finalization */ + +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) + +typedef struct PyInterpreterGuard PyInterpreterGuard; +typedef struct PyInterpreterView PyInterpreterView; + +typedef void PyThreadStateToken; + +PyAPI_FUNC(PyInterpreterGuard *) PyInterpreterGuard_FromCurrent(void); +PyAPI_FUNC(void) PyInterpreterGuard_Close(PyInterpreterGuard *guard); +PyAPI_FUNC(PyInterpreterGuard *) PyInterpreterGuard_FromView(PyInterpreterView *view); + +PyAPI_FUNC(PyInterpreterView *) PyInterpreterView_FromCurrent(void); +PyAPI_FUNC(void) PyInterpreterView_Close(PyInterpreterView *view); +PyAPI_FUNC(PyInterpreterView *) PyInterpreterView_FromMain(void); + +PyAPI_FUNC(PyThreadStateToken *) PyThreadState_Ensure(PyInterpreterGuard *guard); +PyAPI_FUNC(PyThreadStateToken *) PyThreadState_EnsureFromView(PyInterpreterView *view); +PyAPI_FUNC(void) PyThreadState_Release(PyThreadStateToken *tstate); + +#endif + #ifndef Py_LIMITED_API # define Py_CPYTHON_PYSTATE_H # include "cpython/pystate.h" diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pythread.h b/python-multiplatform/src/nativeInterop/cinterop/include/pythread.h index 82247daf8..a8a28b857 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pythread.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pythread.h @@ -42,7 +42,8 @@ PyAPI_FUNC(unsigned long) PyThread_get_thread_ident(void); #if (defined(__APPLE__) || defined(__linux__) || defined(_WIN32) \ || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) \ || defined(__OpenBSD__) || defined(__NetBSD__) \ - || defined(__DragonFly__) || defined(_AIX)) + || defined(__DragonFly__) || defined(_AIX) \ + || (defined(__sun__) && SIZEOF_LONG >= 8)) #define PY_HAVE_THREAD_NATIVE_ID PyAPI_FUNC(unsigned long) PyThread_get_thread_native_id(void); #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/pytypedefs.h b/python-multiplatform/src/nativeInterop/cinterop/include/pytypedefs.h index e78ed56a3..1d0b4e3e8 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/pytypedefs.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/pytypedefs.h @@ -14,6 +14,7 @@ typedef struct PyModuleDef_Slot PyModuleDef_Slot; typedef struct PyMethodDef PyMethodDef; typedef struct PyGetSetDef PyGetSetDef; typedef struct PyMemberDef PyMemberDef; +typedef struct PySlot PySlot; typedef struct _object PyObject; typedef struct _longobject PyLongObject; diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/refcount.h b/python-multiplatform/src/nativeInterop/cinterop/include/refcount.h index 1ef6b2186..80fe7ff70 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/refcount.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/refcount.h @@ -5,6 +5,7 @@ extern "C" { #endif +#if !defined(_Py_OPAQUE_PYOBJECT) /* Immortalization: @@ -19,9 +20,6 @@ immortal. The latter should be the only instances that require cleanup during runtime finalization. */ -#define _Py_STATICALLY_ALLOCATED_FLAG 4 -#define _Py_IMMORTAL_FLAGS 1 - #if SIZEOF_VOID_P > 4 /* In 64+ bit systems, any object whose 32 bit reference count is >= 2**31 @@ -93,14 +91,16 @@ check by comparing the reference count field to the minimum immortality refcount # define _Py_REF_SHARED(refcnt, flags) \ (((refcnt) << _Py_REF_SHARED_SHIFT) + (flags)) #endif // Py_GIL_DISABLED - +#endif // _Py_OPAQUE_PYOBJECT // Py_REFCNT() implementation for the stable ABI PyAPI_FUNC(Py_ssize_t) Py_REFCNT(PyObject *ob); #if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030e0000 // Stable ABI implements Py_REFCNT() as a function call - // on limited C API version 3.14 and newer. + // on limited C API version 3.14 and newer, and on abi3t. +#elif defined(_Py_OPAQUE_PYOBJECT) + // Py_REFCNT() is also a function call in abi3t. #else static inline Py_ssize_t _Py_REFCNT(PyObject *ob) { #if !defined(Py_GIL_DISABLED) @@ -122,13 +122,14 @@ PyAPI_FUNC(Py_ssize_t) Py_REFCNT(PyObject *ob); #endif #endif +#ifndef _Py_OPAQUE_PYOBJECT static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op) { #if defined(Py_GIL_DISABLED) return (_Py_atomic_load_uint32_relaxed(&op->ob_ref_local) == _Py_IMMORTAL_REFCNT_LOCAL); #elif SIZEOF_VOID_P > 4 - return _Py_CAST(PY_INT32_T, op->ob_refcnt) < 0; + return _Py_CAST(int32_t, op->ob_refcnt) < 0; #else return op->ob_refcnt >= _Py_IMMORTAL_MINIMUM_REFCNT; #endif @@ -145,15 +146,17 @@ static inline Py_ALWAYS_INLINE int _Py_IsStaticImmortal(PyObject *op) #endif } #define _Py_IsStaticImmortal(op) _Py_IsStaticImmortal(_PyObject_CAST(op)) +#endif // !defined(_Py_OPAQUE_PYOBJECT) // Py_SET_REFCNT() implementation for stable ABI PyAPI_FUNC(void) _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt); static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) { assert(refcnt >= 0); -#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030d0000 +#if (defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030d0000) \ + || defined(_Py_OPAQUE_PYOBJECT) // Stable ABI implements Py_SET_REFCNT() as a function call - // on limited C API version 3.13 and newer. + // on limited C API version 3.13 and newer, and abi3t. _Py_SetRefcnt(ob, refcnt); #else // This immortal check is for code that is unaware of immortal objects. @@ -165,7 +168,7 @@ static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) { } #ifndef Py_GIL_DISABLED #if SIZEOF_VOID_P > 4 - ob->ob_refcnt = (PY_UINT32_T)refcnt; + ob->ob_refcnt = (uint32_t)refcnt; #else ob->ob_refcnt = refcnt; #endif @@ -192,7 +195,7 @@ static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) { ob->ob_ref_shared = _Py_REF_SHARED(refcnt, _Py_REF_MERGED); } #endif // Py_GIL_DISABLED -#endif // Py_LIMITED_API+0 < 0x030d0000 +#endif // Py_LIMITED_API } #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 # define Py_SET_REFCNT(ob, refcnt) Py_SET_REFCNT(_PyObject_CAST(ob), (refcnt)) @@ -251,10 +254,11 @@ PyAPI_FUNC(void) _Py_DecRef(PyObject *); static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op) { -#if defined(Py_LIMITED_API) && (Py_LIMITED_API+0 >= 0x030c0000 || defined(Py_REF_DEBUG)) +#if (defined(Py_LIMITED_API) && (Py_LIMITED_API+0 >= 0x030c0000 || defined(Py_REF_DEBUG))) \ + || defined(_Py_OPAQUE_PYOBJECT) // Stable ABI implements Py_INCREF() as a function call on limited C API - // version 3.12 and newer, and on Python built in debug mode. _Py_IncRef() - // was added to Python 3.10.0a7, use Py_IncRef() on older Python versions. + // version 3.12 and newer, abi3t, and on Python built in debug mode. + // _Py_IncRef() was added to Python 3.10.0a7, use Py_IncRef() on older versions. // Py_IncRef() accepts NULL whereas _Py_IncRef() doesn't. # if Py_LIMITED_API+0 >= 0x030a00A7 _Py_IncRef(op); @@ -279,7 +283,7 @@ static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op) _Py_atomic_add_ssize(&op->ob_ref_shared, (1 << _Py_REF_SHARED_SHIFT)); } #elif SIZEOF_VOID_P > 4 - PY_UINT32_T cur_refcnt = op->ob_refcnt; + uint32_t cur_refcnt = op->ob_refcnt; if (cur_refcnt >= _Py_IMMORTAL_INITIAL_REFCNT) { // the object is immortal _Py_INCREF_IMMORTAL_STAT_INC(); @@ -306,8 +310,8 @@ static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op) # define Py_INCREF(op) Py_INCREF(_PyObject_CAST(op)) #endif - -#if !defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED) +#if !defined(Py_LIMITED_API) +#if defined(Py_GIL_DISABLED) // Implements Py_DECREF on objects not owned by the current thread. PyAPI_FUNC(void) _Py_DecRefShared(PyObject *); PyAPI_FUNC(void) _Py_DecRefSharedDebug(PyObject *, const char *, int); @@ -317,12 +321,14 @@ PyAPI_FUNC(void) _Py_DecRefSharedDebug(PyObject *, const char *, int); // zero. Otherwise, the thread gives up ownership and merges the reference // count fields. PyAPI_FUNC(void) _Py_MergeZeroLocalRefcount(PyObject *); -#endif +#endif // Py_GIL_DISABLED +#endif // Py_LIMITED_API -#if defined(Py_LIMITED_API) && (Py_LIMITED_API+0 >= 0x030c0000 || defined(Py_REF_DEBUG)) +#if (defined(Py_LIMITED_API) && (Py_LIMITED_API+0 >= 0x030c0000 || defined(Py_REF_DEBUG))) \ + || defined(_Py_OPAQUE_PYOBJECT) // Stable ABI implements Py_DECREF() as a function call on limited C API -// version 3.12 and newer, and on Python built in debug mode. _Py_DecRef() was -// added to Python 3.10.0a7, use Py_DecRef() on older Python versions. +// version 3.12 and newer, abi3t, and on Python built in debug mode. +// _Py_DecRef() was added to Python 3.10.0a7, use Py_DecRef() on older versions. // Py_DecRef() accepts NULL whereas _Py_DecRef() doesn't. static inline void Py_DECREF(PyObject *op) { # if Py_LIMITED_API+0 >= 0x030a00A7 @@ -388,7 +394,7 @@ static inline void Py_DECREF(const char *filename, int lineno, PyObject *op) #if SIZEOF_VOID_P > 4 /* If an object has been freed, it will have a negative full refcnt * If it has not it been freed, will have a very large refcnt */ - if (op->ob_refcnt_full <= 0 || op->ob_refcnt > (((PY_UINT32_T)-1) - (1<<20))) { + if (op->ob_refcnt_full <= 0 || op->ob_refcnt > (((uint32_t)-1) - (1<<20))) { #else if (op->ob_refcnt <= 0) { #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/sliceobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/sliceobject.h index 35e2ea254..9d6a16da9 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/sliceobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/sliceobject.h @@ -16,19 +16,6 @@ PyAPI_DATA(PyObject) _Py_EllipsisObject; /* Don't use this directly */ /* Slice object interface */ -/* - -A slice object containing start, stop, and step data members (the -names are from range). After much talk with Guido, it was decided to -let these be any arbitrary python type. Py_None stands for omitted values. -*/ -#ifndef Py_LIMITED_API -typedef struct { - PyObject_HEAD - PyObject *start, *stop, *step; /* not NULL */ -} PySliceObject; -#endif - PyAPI_DATA(PyTypeObject) PySlice_Type; PyAPI_DATA(PyTypeObject) PyEllipsis_Type; @@ -36,12 +23,6 @@ PyAPI_DATA(PyTypeObject) PyEllipsis_Type; PyAPI_FUNC(PyObject *) PySlice_New(PyObject* start, PyObject* stop, PyObject* step); -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) _PySlice_FromIndices(Py_ssize_t start, Py_ssize_t stop); -PyAPI_FUNC(int) _PySlice_GetLongIndices(PySliceObject *self, PyObject *length, - PyObject **start_ptr, PyObject **stop_ptr, - PyObject **step_ptr); -#endif PyAPI_FUNC(int) PySlice_GetIndices(PyObject *r, Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step); Py_DEPRECATED(3.7) @@ -63,6 +44,12 @@ PyAPI_FUNC(Py_ssize_t) PySlice_AdjustIndices(Py_ssize_t length, Py_ssize_t step); #endif +#ifndef Py_LIMITED_API +# define _Py_CPYTHON_SLICEOBJECT_H +# include "cpython/sliceobject.h" +# undef _Py_CPYTHON_SLICEOBJECT_H +#endif + #ifdef __cplusplus } #endif diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/slots.h b/python-multiplatform/src/nativeInterop/cinterop/include/slots.h new file mode 100644 index 000000000..4bf7bda02 --- /dev/null +++ b/python-multiplatform/src/nativeInterop/cinterop/include/slots.h @@ -0,0 +1,56 @@ +#ifndef _Py_HAVE_SLOTS_H +#define _Py_HAVE_SLOTS_H + +typedef void (*_Py_funcptr_t)(void); + +struct PySlot { + uint16_t sl_id; + uint16_t sl_flags; + _Py_ANONYMOUS union { + uint32_t sl_reserved; // must be 0 + }; + _Py_ANONYMOUS union { + void *sl_ptr; + _Py_funcptr_t sl_func; + Py_ssize_t sl_size; + int64_t sl_int64; + uint64_t sl_uint64; + }; +}; + +#define PySlot_OPTIONAL 0x0001 +#define PySlot_STATIC 0x0002 +#define PySlot_INTPTR 0x0004 + +#define Py_slot_invalid 0xffff + +#define PySlot_DATA(NAME, VALUE) \ + {.sl_id=(NAME), .sl_flags=PySlot_INTPTR, .sl_ptr=(void*)(VALUE)} + +#define PySlot_FUNC(NAME, VALUE) \ + {.sl_id=(NAME), .sl_func=(_Py_funcptr_t)(VALUE)} + +#define PySlot_SIZE(NAME, VALUE) \ + {.sl_id=(NAME), .sl_size=(Py_ssize_t)(VALUE)} + +#define PySlot_INT64(NAME, VALUE) \ + {.sl_id=(NAME), .sl_int64=(int64_t)(VALUE)} + +#define PySlot_UINT64(NAME, VALUE) \ + {.sl_id=(NAME), .sl_uint64=(uint64_t)(VALUE)} + +#define PySlot_STATIC_DATA(NAME, VALUE) \ + {.sl_id=(NAME), .sl_flags=PySlot_STATIC, .sl_ptr=(VALUE)} + +#define PySlot_END {0} + + +// Macros without designated initializers (for C++11 and below): + +#define PySlot_PTR(NAME, VALUE) \ + {(NAME), PySlot_INTPTR, {0}, {(void*)(VALUE)}} + +#define PySlot_PTR_STATIC(NAME, VALUE) \ + {(NAME), PySlot_INTPTR | PySlot_STATIC, {0}, {(void*)(VALUE)}} + +#endif // _Py_HAVE_SLOTS_H diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/typeslots.h b/python-multiplatform/src/nativeInterop/cinterop/include/slots_generated.h similarity index 60% rename from python-multiplatform/src/nativeInterop/cinterop/include/typeslots.h rename to python-multiplatform/src/nativeInterop/cinterop/include/slots_generated.h index a7f3017ec..42edd3ad4 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/typeslots.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/slots_generated.h @@ -1,8 +1,15 @@ -/* Do not renumber the file; these numbers are part of the stable ABI. */ -#define Py_bf_getbuffer 1 -#define Py_bf_releasebuffer 2 -#define Py_mp_ass_subscript 3 -#define Py_mp_length 4 +/* Generated by Tools/build/generate_slots.py */ + +#ifndef _PY_HAVE_SLOTS_GENERATED_H +#define _PY_HAVE_SLOTS_GENERATED_H + +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15) +#define _Py_SLOT_COMPAT_VALUE(OLD, NEW) NEW +#else +#define _Py_SLOT_COMPAT_VALUE(OLD, NEW) OLD +#endif + +#define Py_slot_end 0 #define Py_mp_subscript 5 #define Py_nb_absolute 6 #define Py_nb_add 7 @@ -78,19 +85,37 @@ #define Py_am_await 77 #define Py_am_aiter 78 #define Py_am_anext 79 -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 -/* New in 3.5 */ #define Py_tp_finalize 80 -#endif -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000 -/* New in 3.10 */ #define Py_am_send 81 -#endif -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000 -/* New in 3.14 */ #define Py_tp_vectorcall 82 -#endif -#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000 -/* New in 3.14 */ #define Py_tp_token 83 -#endif +#define Py_mod_create _Py_SLOT_COMPAT_VALUE(1, 84) +#define Py_mod_exec _Py_SLOT_COMPAT_VALUE(2, 85) +#define Py_mod_multiple_interpreters _Py_SLOT_COMPAT_VALUE(3, 86) +#define Py_mod_gil _Py_SLOT_COMPAT_VALUE(4, 87) +#define Py_bf_getbuffer _Py_SLOT_COMPAT_VALUE(1, 88) +#define Py_bf_releasebuffer _Py_SLOT_COMPAT_VALUE(2, 89) +#define Py_mp_ass_subscript _Py_SLOT_COMPAT_VALUE(3, 90) +#define Py_mp_length _Py_SLOT_COMPAT_VALUE(4, 91) +#define Py_slot_subslots 92 +#define Py_tp_slots 93 +#define Py_mod_slots 94 +#define Py_tp_name 95 +#define Py_tp_basicsize 96 +#define Py_tp_extra_basicsize 97 +#define Py_tp_itemsize 98 +#define Py_tp_flags 99 +#define Py_mod_name 100 +#define Py_mod_doc 101 +#define Py_mod_state_size 102 +#define Py_mod_methods 103 +#define Py_mod_state_traverse 104 +#define Py_mod_state_clear 105 +#define Py_mod_state_free 106 +#define Py_tp_metaclass 107 +#define Py_tp_module 108 +#define Py_mod_abi 109 +#define Py_mod_token 110 + +#define _Py_slot_COUNT 111 +#endif /* _PY_HAVE_SLOTS_GENERATED_H */ diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/structseq.h b/python-multiplatform/src/nativeInterop/cinterop/include/structseq.h index 29e24fee5..e5da785f1 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/structseq.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/structseq.h @@ -21,12 +21,6 @@ typedef struct PyStructSequence_Desc { PyAPI_DATA(const char * const) PyStructSequence_UnnamedField; -#ifndef Py_LIMITED_API -PyAPI_FUNC(void) PyStructSequence_InitType(PyTypeObject *type, - PyStructSequence_Desc *desc); -PyAPI_FUNC(int) PyStructSequence_InitType2(PyTypeObject *type, - PyStructSequence_Desc *desc); -#endif PyAPI_FUNC(PyTypeObject*) PyStructSequence_NewType(PyStructSequence_Desc *desc); PyAPI_FUNC(PyObject *) PyStructSequence_New(PyTypeObject* type); @@ -35,9 +29,9 @@ PyAPI_FUNC(void) PyStructSequence_SetItem(PyObject*, Py_ssize_t, PyObject*); PyAPI_FUNC(PyObject*) PyStructSequence_GetItem(PyObject*, Py_ssize_t); #ifndef Py_LIMITED_API -typedef PyTupleObject PyStructSequence; -#define PyStructSequence_SET_ITEM PyStructSequence_SetItem -#define PyStructSequence_GET_ITEM PyStructSequence_GetItem +# define _Py_CPYTHON_STRUCTSEQ_H +# include "cpython/structseq.h" +# undef _Py_CPYTHON_STRUCTSEQ_H #endif #ifdef __cplusplus diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/sysmodule.h b/python-multiplatform/src/nativeInterop/cinterop/include/sysmodule.h index c1d5f610f..b7d800c5e 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/sysmodule.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/sysmodule.h @@ -4,6 +4,12 @@ extern "C" { #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030f0000 +PyAPI_FUNC(PyObject *) PySys_GetAttr(PyObject *); +PyAPI_FUNC(PyObject *) PySys_GetAttrString(const char *); +PyAPI_FUNC(int) PySys_GetOptionalAttr(PyObject *, PyObject **); +PyAPI_FUNC(int) PySys_GetOptionalAttrString(const char *, PyObject **); +#endif PyAPI_FUNC(PyObject *) PySys_GetObject(const char *); PyAPI_FUNC(int) PySys_SetObject(const char *, PyObject *); @@ -17,8 +23,6 @@ PyAPI_FUNC(void) PySys_WriteStderr(const char *format, ...) PyAPI_FUNC(void) PySys_FormatStdout(const char *format, ...); PyAPI_FUNC(void) PySys_FormatStderr(const char *format, ...); -Py_DEPRECATED(3.13) PyAPI_FUNC(void) PySys_ResetWarnOptions(void); - PyAPI_FUNC(PyObject *) PySys_GetXOptions(void); #ifdef __cplusplus diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/unicodeobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/unicodeobject.h index f8bcaecb9..29f1d1b01 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/unicodeobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/unicodeobject.h @@ -64,13 +64,9 @@ Copyright (c) Corporation for National Research Initiatives. #error Must define SIZEOF_WCHAR_T #endif +/* Soft-deprecated defines */ #define Py_UNICODE_SIZE SIZEOF_WCHAR_T - -/* If wchar_t can be used for UCS-4 storage, set Py_UNICODE_WIDE. - Otherwise, Unicode strings are stored as UCS-2 (with limited support - for UTF-16) */ - -#if Py_UNICODE_SIZE >= 4 +#if SIZEOF_WCHAR_T >= 4 #define Py_UNICODE_WIDE #endif @@ -341,49 +337,6 @@ PyAPI_FUNC(PyObject*) PyUnicode_Decode( const char *errors /* error handling */ ); -/* Decode a Unicode object unicode and return the result as Python - object. - - This API is DEPRECATED and will be removed in 3.15. - The only supported standard encoding is rot13. - Use PyCodec_Decode() to decode with rot13 and non-standard codecs - that decode from str. */ - -Py_DEPRECATED(3.6) PyAPI_FUNC(PyObject*) PyUnicode_AsDecodedObject( - PyObject *unicode, /* Unicode object */ - const char *encoding, /* encoding */ - const char *errors /* error handling */ - ); - -/* Decode a Unicode object unicode and return the result as Unicode - object. - - This API is DEPRECATED and will be removed in 3.15. - The only supported standard encoding is rot13. - Use PyCodec_Decode() to decode with rot13 and non-standard codecs - that decode from str to str. */ - -Py_DEPRECATED(3.6) PyAPI_FUNC(PyObject*) PyUnicode_AsDecodedUnicode( - PyObject *unicode, /* Unicode object */ - const char *encoding, /* encoding */ - const char *errors /* error handling */ - ); - -/* Encodes a Unicode object and returns the result as Python - object. - - This API is DEPRECATED and will be removed in 3.15. - It is superseded by PyUnicode_AsEncodedString() - since all standard encodings (except rot13) encode str to bytes. - Use PyCodec_Encode() for encoding with rot13 and non-standard codecs - that encode form str to non-bytes. */ - -Py_DEPRECATED(3.6) PyAPI_FUNC(PyObject*) PyUnicode_AsEncodedObject( - PyObject *unicode, /* Unicode object */ - const char *encoding, /* encoding */ - const char *errors /* error handling */ - ); - /* Encodes a Unicode object and returns the result as Python string object. */ @@ -393,20 +346,6 @@ PyAPI_FUNC(PyObject*) PyUnicode_AsEncodedString( const char *errors /* error handling */ ); -/* Encodes a Unicode object and returns the result as Unicode - object. - - This API is DEPRECATED and will be removed in 3.15. - The only supported standard encodings is rot13. - Use PyCodec_Encode() to encode with rot13 and non-standard codecs - that encode from str to str. */ - -Py_DEPRECATED(3.6) PyAPI_FUNC(PyObject*) PyUnicode_AsEncodedUnicode( - PyObject *unicode, /* Unicode object */ - const char *encoding, /* encoding */ - const char *errors /* error handling */ - ); - /* Build an encoding map. */ PyAPI_FUNC(PyObject*) PyUnicode_BuildEncodingMap( diff --git a/python-multiplatform/src/nativeInterop/cinterop/include/weakrefobject.h b/python-multiplatform/src/nativeInterop/cinterop/include/weakrefobject.h index a6e71eb17..17fac6296 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/include/weakrefobject.h +++ b/python-multiplatform/src/nativeInterop/cinterop/include/weakrefobject.h @@ -27,7 +27,6 @@ PyAPI_FUNC(PyObject *) PyWeakref_NewRef(PyObject *ob, PyObject *callback); PyAPI_FUNC(PyObject *) PyWeakref_NewProxy(PyObject *ob, PyObject *callback); -Py_DEPRECATED(3.13) PyAPI_FUNC(PyObject *) PyWeakref_GetObject(PyObject *ref); #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030D0000 PyAPI_FUNC(int) PyWeakref_GetRef(PyObject *ref, PyObject **pobj); diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/idlelib/help.html b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/idlelib/help.html index 827d230b5..480d32e60 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/idlelib/help.html +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/idlelib/help.html @@ -25,7 +25,7 @@ - + @@ -135,7 +135,7 @@

    Table of Contents

    Previous topic

    tkinter.ttk — Tk themed widgets

    + title="previous chapter">tkinter.ttk, Tk themed widgets

    Next topic

    @@ -171,7 +171,7 @@

    Navigation

    next |
  • - previous |
  • python logo
  • @@ -1084,7 +1084,7 @@

    Table of Contents

    Previous topic

    tkinter.ttk — Tk themed widgets

    + title="previous chapter">tkinter.ttk, Tk themed widgets

    Next topic

    @@ -1123,7 +1123,7 @@

    Navigation

    next |
  • - previous |
  • python logo
  • diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/pydoc_data/topics.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/pydoc_data/topics.py index dbfeb5599..171e3d22b 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/pydoc_data/topics.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/pydoc_data/topics.py @@ -1703,7 +1703,7 @@ 'exception. How this value is computed depends on the type of the\n' 'callable object.\n' '\n' - 'If it is—\n' + 'If it is, \n' '\n' 'a user-defined function:\n' ' The code block for the function is executed, passing it the\n' @@ -1958,7 +1958,7 @@ 'most\n' 'important built-in types.\n' '\n' - '* Numbers of built-in numeric types (Numeric Types — int, ' + '* Numbers of built-in numeric types (Numeric Types, int, ' 'float,\n' ' complex) and of the standard library types ' '"fractions.Fraction" and\n' @@ -3647,7 +3647,7 @@ 'be omitted from a call, in which case the parameter’s default ' 'value is\n' 'substituted. If a parameter has a default value, all following\n' - 'parameters up until the “"*"” must also have a default value — ' + 'parameters up until the “"*"” must also have a default value, ' 'this is\n' 'a syntactic restriction that is not expressed by the grammar.\n' '\n' @@ -4600,7 +4600,7 @@ '\n' ' Note:\n' '\n' - ' "del x" doesn’t directly call "x.__del__()" — the ' + ' "del x" doesn’t directly call "x.__del__()", the ' 'former\n' ' decrements the reference count for "x" by one, and the ' 'latter is\n' @@ -4959,7 +4959,7 @@ ' neither "__len__()" nor "__bool__()", all its instances ' 'are\n' ' considered true.\n', - 'debugger': '"pdb" — The Python Debugger\n' + 'debugger': '"pdb", The Python Debugger\n' '***************************\n' '\n' '**Source code:** Lib/pdb.py\n' @@ -5486,7 +5486,7 @@ ' followed by end). This is because any time you resume ' 'execution\n' ' (even with a simple next or step), you may encounter another\n' - ' breakpoint—which could have its own command list, leading to\n' + ' breakpoint, which could have its own command list, leading to\n' ' ambiguities about which list to execute.\n' '\n' ' If you use the "silent" command in the command list, the ' @@ -5598,7 +5598,7 @@ '\n' ' Note:\n' '\n' - ' "print()" can also be used, but is not a debugger command — ' + ' "print()" can also be used, but is not a debugger command, ' 'this\n' ' executes the Python "print()" function.\n' '\n' @@ -7559,7 +7559,7 @@ 'be omitted from a call, in which case the parameter’s default ' 'value is\n' 'substituted. If a parameter has a default value, all following\n' - 'parameters up until the “"*"” must also have a default value — ' + 'parameters up until the “"*"” must also have a default value, ' 'this is\n' 'a syntactic restriction that is not expressed by the grammar.\n' '\n' @@ -8143,7 +8143,7 @@ 'were\n' 'imported and used within the module).\n' '\n' - 'The wild card form of import — "from module import *" — is only\n' + 'The wild card form of import, "from module import *", is only\n' 'allowed at the module level. Attempting to use it in class or\n' 'function definitions will raise a "SyntaxError".\n' '\n' @@ -8270,7 +8270,7 @@ 'the\n' 'future statement. This can be controlled by optional arguments ' 'to\n' - '"compile()" — see the documentation of that function for details.\n' + '"compile()", see the documentation of that function for details.\n' '\n' 'A future statement typed at an interactive interpreter prompt ' 'will\n' @@ -9075,7 +9075,7 @@ 'Objects are never explicitly destroyed; however, when they ' 'become\n' 'unreachable they may be garbage-collected. An implementation is\n' - 'allowed to postpone garbage collection or omit it altogether — it ' + 'allowed to postpone garbage collection or omit it altogether, it ' 'is a\n' 'matter of implementation quality how garbage collection is\n' 'implemented, as long as no objects are collected that are still\n' @@ -9341,7 +9341,7 @@ '\n' ' pass_stmt ::= "pass"\n' '\n' - '"pass" is a null operation — when it is executed, nothing happens. ' + '"pass" is a null operation, when it is executed, nothing happens. ' 'It\n' 'is useful as a placeholder when a statement is required ' 'syntactically,\n' @@ -10055,7 +10055,7 @@ '\n' ' Note:\n' '\n' - ' "del x" doesn’t directly call "x.__del__()" — the ' + ' "del x" doesn’t directly call "x.__del__()", the ' 'former\n' ' decrements the reference count for "x" by one, and the ' 'latter is\n' @@ -15288,7 +15288,7 @@ 'different object types.\n' '\n' 'See Function definitions for more information.\n', - 'typesmapping': 'Mapping Types — "dict"\n' + 'typesmapping': 'Mapping Types, "dict"\n' '**********************\n' '\n' 'A *mapping* object maps *hashable* values to arbitrary ' @@ -15875,7 +15875,7 @@ 'written as\n' '"".\n', - 'typesseq': 'Sequence Types — "list", "tuple", "range"\n' + 'typesseq': 'Sequence Types, "list", "tuple", "range"\n' '*****************************************\n' '\n' 'There are three basic sequence types: lists, tuples, and range\n' @@ -16361,7 +16361,7 @@ 'is\n' ' stable if it guarantees not to change the relative order ' 'of\n' - ' elements that compare equal — this is helpful for sorting ' + ' elements that compare equal, this is helpful for sorting ' 'in\n' ' multiple passes (for example, sort by department, then by ' 'salary\n' @@ -16542,7 +16542,7 @@ 'Range objects implement the "collections.abc.Sequence" ABC, and\n' 'provide features such as containment tests, element index ' 'lookup,\n' - 'slicing and support for negative indices (see Sequence Types — ' + 'slicing and support for negative indices (see Sequence Types, ' 'list,\n' 'tuple, range):\n' '\n' diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/_support.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/_support.py index 5a76c163f..2b00cba15 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/_support.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/_support.py @@ -30,7 +30,7 @@ def get_modules(): Because this function manipulates the import system in a way that might be fragile or do unexpected things if it is run many times, it uses a `call_once` decorator to ensure that this is only ever called exactly - one time — in other words, when using this function you will only ever + one time, in other words, when using this function you will only ever get one copy of each module rather than a fresh import each time. """ import zoneinfo as c_module @@ -80,7 +80,7 @@ def pop_tzdata_modules(): with lock: if block_tzdata: # In order to fully exclude tzdata from the path, we need to - # clear the sys.modules cache of all its contents — setting the + # clear the sys.modules cache of all its contents, setting the # root package to None is not enough to block direct access of # already-imported submodules (though it will prevent new # imports of submodules). diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo.py index 8bcd6d2e9..b36653a01 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo.py @@ -812,7 +812,7 @@ def construct_zone(self, transitions, after=None, version=3): def zt_as_tuple(zt): # zt may be a tuple (timestamp, offset_before, offset_after) or - # a ZoneTransition object — this is to allow the timestamp to be + # a ZoneTransition object, this is to allow the timestamp to be # values that are outside the valid range for datetimes but still # valid 64-bit timestamps. if isinstance(zt, tuple): diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo_property.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo_property.py index feaa77f3e..ffe66c4a8 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo_property.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/arm64-v8a/python3.13/test/test_zoneinfo/test_zoneinfo_property.py @@ -41,7 +41,7 @@ def valid_key(key): except ModuleNotFoundError: return False - # This relies on the fact that dictionaries maintain insertion order — for + # This relies on the fact that dictionaries maintain insertion order, for # shrinking purposes, it is preferable to start with the standard version, # then move to the posix/ version, then to the right/ version. out_zones = {"": available_zones} diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/idlelib/help.html b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/idlelib/help.html index 827d230b5..480d32e60 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/idlelib/help.html +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/idlelib/help.html @@ -25,7 +25,7 @@ - + @@ -135,7 +135,7 @@

    Table of Contents

    Previous topic

    tkinter.ttk — Tk themed widgets

    + title="previous chapter">tkinter.ttk, Tk themed widgets

    Next topic

    @@ -171,7 +171,7 @@

    Navigation

    next |
  • - previous |
  • python logo
  • @@ -1084,7 +1084,7 @@

    Table of Contents

    Previous topic

    tkinter.ttk — Tk themed widgets

    + title="previous chapter">tkinter.ttk, Tk themed widgets

    Next topic

    @@ -1123,7 +1123,7 @@

    Navigation

    next |
  • - previous |
  • python logo
  • diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/pydoc_data/topics.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/pydoc_data/topics.py index dbfeb5599..171e3d22b 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/pydoc_data/topics.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/pydoc_data/topics.py @@ -1703,7 +1703,7 @@ 'exception. How this value is computed depends on the type of the\n' 'callable object.\n' '\n' - 'If it is—\n' + 'If it is, \n' '\n' 'a user-defined function:\n' ' The code block for the function is executed, passing it the\n' @@ -1958,7 +1958,7 @@ 'most\n' 'important built-in types.\n' '\n' - '* Numbers of built-in numeric types (Numeric Types — int, ' + '* Numbers of built-in numeric types (Numeric Types, int, ' 'float,\n' ' complex) and of the standard library types ' '"fractions.Fraction" and\n' @@ -3647,7 +3647,7 @@ 'be omitted from a call, in which case the parameter’s default ' 'value is\n' 'substituted. If a parameter has a default value, all following\n' - 'parameters up until the “"*"” must also have a default value — ' + 'parameters up until the “"*"” must also have a default value, ' 'this is\n' 'a syntactic restriction that is not expressed by the grammar.\n' '\n' @@ -4600,7 +4600,7 @@ '\n' ' Note:\n' '\n' - ' "del x" doesn’t directly call "x.__del__()" — the ' + ' "del x" doesn’t directly call "x.__del__()", the ' 'former\n' ' decrements the reference count for "x" by one, and the ' 'latter is\n' @@ -4959,7 +4959,7 @@ ' neither "__len__()" nor "__bool__()", all its instances ' 'are\n' ' considered true.\n', - 'debugger': '"pdb" — The Python Debugger\n' + 'debugger': '"pdb", The Python Debugger\n' '***************************\n' '\n' '**Source code:** Lib/pdb.py\n' @@ -5486,7 +5486,7 @@ ' followed by end). This is because any time you resume ' 'execution\n' ' (even with a simple next or step), you may encounter another\n' - ' breakpoint—which could have its own command list, leading to\n' + ' breakpoint, which could have its own command list, leading to\n' ' ambiguities about which list to execute.\n' '\n' ' If you use the "silent" command in the command list, the ' @@ -5598,7 +5598,7 @@ '\n' ' Note:\n' '\n' - ' "print()" can also be used, but is not a debugger command — ' + ' "print()" can also be used, but is not a debugger command, ' 'this\n' ' executes the Python "print()" function.\n' '\n' @@ -7559,7 +7559,7 @@ 'be omitted from a call, in which case the parameter’s default ' 'value is\n' 'substituted. If a parameter has a default value, all following\n' - 'parameters up until the “"*"” must also have a default value — ' + 'parameters up until the “"*"” must also have a default value, ' 'this is\n' 'a syntactic restriction that is not expressed by the grammar.\n' '\n' @@ -8143,7 +8143,7 @@ 'were\n' 'imported and used within the module).\n' '\n' - 'The wild card form of import — "from module import *" — is only\n' + 'The wild card form of import, "from module import *", is only\n' 'allowed at the module level. Attempting to use it in class or\n' 'function definitions will raise a "SyntaxError".\n' '\n' @@ -8270,7 +8270,7 @@ 'the\n' 'future statement. This can be controlled by optional arguments ' 'to\n' - '"compile()" — see the documentation of that function for details.\n' + '"compile()", see the documentation of that function for details.\n' '\n' 'A future statement typed at an interactive interpreter prompt ' 'will\n' @@ -9075,7 +9075,7 @@ 'Objects are never explicitly destroyed; however, when they ' 'become\n' 'unreachable they may be garbage-collected. An implementation is\n' - 'allowed to postpone garbage collection or omit it altogether — it ' + 'allowed to postpone garbage collection or omit it altogether, it ' 'is a\n' 'matter of implementation quality how garbage collection is\n' 'implemented, as long as no objects are collected that are still\n' @@ -9341,7 +9341,7 @@ '\n' ' pass_stmt ::= "pass"\n' '\n' - '"pass" is a null operation — when it is executed, nothing happens. ' + '"pass" is a null operation, when it is executed, nothing happens. ' 'It\n' 'is useful as a placeholder when a statement is required ' 'syntactically,\n' @@ -10055,7 +10055,7 @@ '\n' ' Note:\n' '\n' - ' "del x" doesn’t directly call "x.__del__()" — the ' + ' "del x" doesn’t directly call "x.__del__()", the ' 'former\n' ' decrements the reference count for "x" by one, and the ' 'latter is\n' @@ -15288,7 +15288,7 @@ 'different object types.\n' '\n' 'See Function definitions for more information.\n', - 'typesmapping': 'Mapping Types — "dict"\n' + 'typesmapping': 'Mapping Types, "dict"\n' '**********************\n' '\n' 'A *mapping* object maps *hashable* values to arbitrary ' @@ -15875,7 +15875,7 @@ 'written as\n' '"".\n', - 'typesseq': 'Sequence Types — "list", "tuple", "range"\n' + 'typesseq': 'Sequence Types, "list", "tuple", "range"\n' '*****************************************\n' '\n' 'There are three basic sequence types: lists, tuples, and range\n' @@ -16361,7 +16361,7 @@ 'is\n' ' stable if it guarantees not to change the relative order ' 'of\n' - ' elements that compare equal — this is helpful for sorting ' + ' elements that compare equal, this is helpful for sorting ' 'in\n' ' multiple passes (for example, sort by department, then by ' 'salary\n' @@ -16542,7 +16542,7 @@ 'Range objects implement the "collections.abc.Sequence" ABC, and\n' 'provide features such as containment tests, element index ' 'lookup,\n' - 'slicing and support for negative indices (see Sequence Types — ' + 'slicing and support for negative indices (see Sequence Types, ' 'list,\n' 'tuple, range):\n' '\n' diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/_support.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/_support.py index 5a76c163f..2b00cba15 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/_support.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/_support.py @@ -30,7 +30,7 @@ def get_modules(): Because this function manipulates the import system in a way that might be fragile or do unexpected things if it is run many times, it uses a `call_once` decorator to ensure that this is only ever called exactly - one time — in other words, when using this function you will only ever + one time, in other words, when using this function you will only ever get one copy of each module rather than a fresh import each time. """ import zoneinfo as c_module @@ -80,7 +80,7 @@ def pop_tzdata_modules(): with lock: if block_tzdata: # In order to fully exclude tzdata from the path, we need to - # clear the sys.modules cache of all its contents — setting the + # clear the sys.modules cache of all its contents, setting the # root package to None is not enough to block direct access of # already-imported submodules (though it will prevent new # imports of submodules). diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo.py index 8bcd6d2e9..b36653a01 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo.py @@ -812,7 +812,7 @@ def construct_zone(self, transitions, after=None, version=3): def zt_as_tuple(zt): # zt may be a tuple (timestamp, offset_before, offset_after) or - # a ZoneTransition object — this is to allow the timestamp to be + # a ZoneTransition object, this is to allow the timestamp to be # values that are outside the valid range for datetimes but still # valid 64-bit timestamps. if isinstance(zt, tuple): diff --git a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo_property.py b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo_property.py index feaa77f3e..ffe66c4a8 100644 --- a/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo_property.py +++ b/python-multiplatform/src/nativeInterop/cinterop/lib/android/x86_64/python3.13/test/test_zoneinfo/test_zoneinfo_property.py @@ -41,7 +41,7 @@ def valid_key(key): except ModuleNotFoundError: return False - # This relies on the fact that dictionaries maintain insertion order — for + # This relies on the fact that dictionaries maintain insertion order, for # shrinking purposes, it is preferable to start with the standard version, # then move to the posix/ version, then to the right/ version. out_zones = {"": available_zones} diff --git a/python-multiplatform/src/nativeMain/README.md b/python-multiplatform/src/nativeMain/README.md index 489b3c0d8..3b0c7e721 100644 --- a/python-multiplatform/src/nativeMain/README.md +++ b/python-multiplatform/src/nativeMain/README.md @@ -1,9 +1,9 @@ -# nativeMain — rules +# nativeMain, rules Kotlin/Native sources shared by iOS **and** androidNative. Reaches CPython through cinterop, in the same process, with no boundary to cross. -## Shared with iOS — check before adding anything +## Shared with iOS, check before adding anything `artMain` (androidNative-only) depends on this, and so does `iosMain`. Anything added here is compiled for iPhones too. JNI exports, `@CName` symbols intended for ART, and anything else @@ -12,7 +12,7 @@ Android-shaped belong in `artMain`. ## There is no boundary here Kotlin and CPython are in one binary. A call is a call. This is the fastest platform in the -project for FFI, and composition — which exists to avoid crossings — buys nothing. `actual` +project for FFI, and composition, which exists to avoid crossings, buys nothing. `actual` implementations of composed operations should just do the work directly. The same is true in the other direction: an upcall from Python into Kotlin is a plain function @@ -23,13 +23,13 @@ call, not a runtime transition. The upcall cost that shapes the JVM design does `UpcallEntry.kt` is the entry point Python calls in. It does **not** use the `@CName` + `ctypes.CDLL(None)` route the design doc predicted, because neither half of that survives on iOS: the `Python.framework` here has no `_ctypes` (no `lib-dynload` at all), and a `@CName` alias is -never emitted into a Kotlin/Native framework or test executable — only into the androidNative +never emitted into a Kotlin/Native framework or test executable, only into the androidNative `.so`. Measurements are in `docs/design/upcall.md`. So an entry point that Python must be able to call needs a `PyMethodDef` with a `staticCFunction` in `ml_meth`. Two rules come with that, and both have crashed this repo before: -- The `PyMethodDef` and its `ml_name` must outlive every callable built from it — `nativeHeap`, +- The `PyMethodDef` and its `ml_name` must outlive every callable built from it, `nativeHeap`, never freed. A `PyCFunction` object stores the `PyMethodDef *`; it does not copy it. - An entry point reached from C takes its own `PyGILState_Ensure` **unconditionally**. `withGIL`'s depth counter records scopes Kotlin opened, and C is free to have dropped the GIL inside one. diff --git a/python-multiplatform/src/test/python/test_devagent.py b/python-multiplatform/src/test/python/test_devagent.py new file mode 100644 index 000000000..309caa7f5 --- /dev/null +++ b/python-multiplatform/src/test/python/test_devagent.py @@ -0,0 +1,348 @@ +"""The dev agent's protocol (#233; SPEC L-12), with no UI and no Kotlin: `python3 -m unittest` from this +directory. The agent is `src/commonMain/python/python_multiplatform_devagent.py`, the same file the runtime +embeds. + +Each test starts an agent on an ephemeral loopback port in this process (as the app would, in its own +interpreter) and talks to it over a real socket, one JSON object per line. +""" +import json +import os +import socket +import sys +import tempfile +import textwrap +import types +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(HERE, "..", "..", "commonMain", "python")) + +import python_multiplatform_devagent as devagent # noqa: E402 + +# secrets.token_urlsafe(32): the shortest token the agent accepts is this long. +TOKEN = "dGVzdC10b2tlbi1mb3ItdGhlLWRldi1hZ2VudC10ZXN0cw" + + +class Client: + def __init__(self, port: int): + self.sock = socket.create_connection(("127.0.0.1", port), timeout=10) + self.reader = self.sock.makefile("r", encoding="utf-8") + self.next_id = 0 + + def send(self, op: str, **fields) -> int: + self.next_id += 1 + message = {"id": self.next_id, "op": op, **fields} + self.sock.sendall((json.dumps(message) + "\n").encode("utf-8")) + return self.next_id + + def read(self) -> dict: + line = self.reader.readline() + if not line: + raise ConnectionError("the agent closed the connection") + return json.loads(line) + + def request(self, op: str, **fields) -> list: + """Every message answering this request, up to and including its `done`.""" + request_id = self.send(op, **fields) + replies = [] + while True: + reply = self.read() + self.assert_id(reply, request_id) + replies.append(reply) + if reply["type"] in ("done", "hello", "error"): + return replies + + @staticmethod + def assert_id(reply: dict, request_id: int): + if reply.get("id") != request_id: + raise AssertionError(f"reply {reply} does not answer request {request_id}") + + def close(self): + self.reader.close() + self.sock.close() + + +class AgentTestCase(unittest.TestCase): + def setUp(self): + self.overlay = tempfile.mkdtemp(prefix="devagent-overlay-") + self.agent = devagent.start(port=0, token=TOKEN, overlay=self.overlay) + self.client = Client(self.agent.port) + hello = self.client.request("hello", token=TOKEN) + self.assertEqual("hello", hello[-1]["type"]) + + def tearDown(self): + self.client.close() + self.agent.stop() + for name in [n for n in sys.modules if n.startswith("devagent_fixture")]: + del sys.modules[name] + if self.overlay in sys.path: + sys.path.remove(self.overlay) + + def execute(self, code: str) -> list: + return self.client.request("execute", code=textwrap.dedent(code)) + + @staticmethod + def done(replies: list) -> dict: + return replies[-1] + + @staticmethod + def of_type(replies: list, kind: str) -> list: + return [r for r in replies if r["type"] == kind] + + +class Authentication(unittest.TestCase): + def test_a_wrong_token_is_refused_and_the_connection_closed(self): + agent = devagent.start(port=0, token=TOKEN, overlay=tempfile.mkdtemp()) + try: + client = Client(agent.port) + reply = client.request("hello", token="wrong")[-1] + self.assertEqual("error", reply["type"]) + self.assertEqual("AuthError", reply["ename"]) + self.assertEqual("", client.reader.readline(), "the agent closes the connection") + client.close() + finally: + agent.stop() + + def test_a_request_before_hello_is_refused(self): + agent = devagent.start(port=0, token=TOKEN, overlay=tempfile.mkdtemp()) + try: + client = Client(agent.port) + reply = client.request("execute", code="1")[-1] + self.assertEqual("AuthError", reply["ename"]) + client.close() + finally: + agent.stop() + + def test_it_listens_on_loopback_only(self): + agent = devagent.start(port=0, token=TOKEN, overlay=tempfile.mkdtemp()) + try: + self.assertEqual("127.0.0.1", agent.host) + finally: + agent.stop() + + def test_the_request_comes_from_the_environment(self): + self.assertEqual( + {"port": 4719, "token": TOKEN, "overlay": "/tmp/o"}, + devagent.parse_request(f"port=4719;token={TOKEN};overlay=/tmp/o"), + ) + self.assertEqual({"port": 4719, "token": TOKEN, "overlay": None}, devagent.parse_request(f"port=4719;token={TOKEN}")) + for bad in ["", f"port=x;token={TOKEN}", "port=1", f"token={TOKEN}"]: + with self.assertRaises(ValueError, msg=bad): + devagent.parse_request(bad) + + def test_a_short_token_is_refused_before_anything_listens(self): + short = TOKEN[: devagent.MIN_TOKEN_LENGTH - 1] + with self.assertRaises(ValueError): + devagent.parse_request(f"port=4719;token={short}") + with self.assertRaises(ValueError): + devagent.start(port=0, token=short, overlay=tempfile.mkdtemp()) + self.assertGreaterEqual(devagent.MIN_TOKEN_LENGTH, len(__import__("secrets").token_urlsafe(32))) + + def test_another_socket_cannot_take_the_port_over(self): + agent = devagent.start(port=0, token=TOKEN, overlay=tempfile.mkdtemp()) + try: + options = [socket.SO_REUSEADDR] + ([socket.SO_REUSEPORT] if hasattr(socket, "SO_REUSEPORT") else []) + for option in options: + hijacker = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + hijacker.setsockopt(socket.SOL_SOCKET, option, 1) + with self.assertRaises(OSError, msg=f"option {option}"): + hijacker.bind(("127.0.0.1", agent.port)) + finally: + hijacker.close() + finally: + agent.stop() + + def test_a_restarted_agent_gets_its_port_back(self): + # A launcher reuses the port across app restarts, so the closed agent's connections in TIME_WAIT + # must not keep the next one from binding. + agent = devagent.start(port=0, token=TOKEN, overlay=tempfile.mkdtemp()) + port = agent.port + client = Client(port) + client.request("hello", token=TOKEN) + agent.stop() + client.close() + again = devagent.start(port=port, token=TOKEN, overlay=tempfile.mkdtemp()) + again.stop() + + +class Execute(AgentTestCase): + def test_the_hello_reply_names_the_protocol(self): + client = Client(self.agent.port) + hello = client.request("hello", token=TOKEN)[-1] + self.assertEqual(1, hello["protocol"]) + self.assertIn("python", hello) + client.close() + + def test_the_last_expression_is_the_result(self): + self.execute("x_devagent = 40") + replies = self.execute("x_devagent + 2") + self.assertEqual("ok", self.done(replies)["status"]) + self.assertEqual("42", self.of_type(replies, "result")[0]["data"]["text/plain"]) + + def test_the_namespace_is_main_and_persists(self): + self.execute("import sys\nsys.modules['__main__'].devagent_marker = 'kept'") + replies = self.execute("devagent_marker") + self.assertEqual("'kept'", self.of_type(replies, "result")[0]["data"]["text/plain"]) + + def test_stdout_and_stderr_are_streamed(self): + replies = self.execute("import sys\nprint('out line')\nprint('err line', file=sys.stderr)") + streams = {r["name"]: "" for r in self.of_type(replies, "stream")} + for r in self.of_type(replies, "stream"): + streams[r["name"]] += r["text"] + self.assertEqual("out line\n", streams["stdout"]) + self.assertEqual("err line\n", streams["stderr"]) + self.assertEqual([], self.of_type(replies, "result"), "a print returns nothing") + + def test_an_error_is_reported_with_its_traceback(self): + replies = self.execute("1 / 0") + done = self.done(replies) + self.assertEqual("error", done["status"]) + self.assertEqual("ZeroDivisionError", done["ename"]) + self.assertTrue(any("ZeroDivisionError" in line for line in done["traceback"]), done["traceback"]) + + def test_a_syntax_error_is_reported(self): + done = self.done(self.execute("def (")) + self.assertEqual("SyntaxError", done["ename"]) + + def test_the_execution_count_grows(self): + first = self.done(self.execute("1"))["count"] + second = self.done(self.execute("2"))["count"] + self.assertEqual(first + 1, second) + + def test_completion_offers_names_in_the_namespace(self): + self.execute("devagent_completion_target = 1") + done = self.done(self.client.request("complete", code="devagent_comp", cursor_pos=13)) + self.assertIn("devagent_completion_target", done["matches"]) + self.assertEqual(0, done["cursor_start"]) + self.assertEqual(13, done["cursor_end"]) + + def test_an_unknown_op_is_an_error_and_the_connection_stays(self): + done = self.done(self.client.request("teleport")) + self.assertEqual("error", done["status"]) + self.assertEqual("ok", self.done(self.execute("1"))["status"]) + + +class Push(AgentTestCase): + def test_pushed_files_land_in_the_overlay_which_is_first_on_sys_path(self): + done = self.done(self.client.request("push", files={"devagent_fixture_a.py": "VALUE = 1\n"})) + self.assertEqual("ok", done["status"]) + self.assertEqual(self.overlay, sys.path[0]) + self.assertTrue(os.path.isfile(os.path.join(self.overlay, "devagent_fixture_a.py"))) + replies = self.execute("import devagent_fixture_a\ndevagent_fixture_a.VALUE") + self.assertEqual("1", self.of_type(replies, "result")[0]["data"]["text/plain"]) + + def test_a_loaded_module_is_reimported_and_reported(self): + self.client.request("push", files={"devagent_fixture_b.py": "VALUE = 1\n"}) + self.execute("import devagent_fixture_b") + done = self.done(self.client.request("push", files={"devagent_fixture_b.py": "VALUE = 2\n"})) + self.assertEqual("ok", done["status"]) + self.assertEqual(["devagent_fixture_b"], done["reloaded"]) + replies = self.execute("devagent_fixture_b.VALUE") + self.assertEqual("2", self.of_type(replies, "result")[0]["data"]["text/plain"]) + + def test_a_package_module_and_its_package_are_reimported_children_first(self): + files = {"devagent_fixture_pkg/__init__.py": "", "devagent_fixture_pkg/mod.py": "VALUE = 1\n"} + self.client.request("push", files=files) + self.execute("import devagent_fixture_pkg.mod") + done = self.done(self.client.request("push", files={"devagent_fixture_pkg/mod.py": "VALUE = 3\n"})) + self.assertEqual(["devagent_fixture_pkg.mod"], done["reloaded"]) + replies = self.execute("devagent_fixture_pkg.mod.VALUE") + self.assertEqual("3", self.of_type(replies, "result")[0]["data"]["text/plain"]) + + def test_a_reload_that_fails_keeps_the_old_module(self): + self.client.request("push", files={"devagent_fixture_c.py": "VALUE = 1\n"}) + self.execute("import devagent_fixture_c") + done = self.done(self.client.request("push", files={"devagent_fixture_c.py": "VALUE = (\n"})) + self.assertEqual("error", done["status"]) + self.assertEqual("SyntaxError", done["ename"]) + replies = self.execute("devagent_fixture_c.VALUE") + self.assertEqual("1", self.of_type(replies, "result")[0]["data"]["text/plain"]) + + def test_a_path_outside_the_overlay_is_refused(self): + # Windows forms too, on every OS: on Windows `os.path.isabs("/abs/escape.py")` is False (Python 3.13+) + # and joining it to the overlay lands on the drive root, so the rule cannot be os.path's. + for path in ["../escape.py", "/abs/escape.py", "a/../../escape.py", "C:/escape.py", "C:escape.py", + "\\escape.py", "a\\..\\..\\escape.py", "//server/share/escape.py", "./a.py", "a//b.py", ""]: + done = self.done(self.client.request("push", files={path: "X = 1\n"})) + self.assertEqual("error", done["status"], path) + self.assertEqual("ValueError", done["ename"], path) + + +class ImportGate(unittest.TestCase): + """N-7b: when `typedpython.importgate` is in the app, a reload goes through it.""" + + def setUp(self): + self.installed = [] + gate_errors = [] + + class ImportGateError(ImportError): + pass + + class Finder: + """Refuses any module under a root whose source contains the word REJECT.""" + + def __init__(self, roots): + self.roots = roots + + def find_spec(self, name, path=None, target=None): + import importlib.machinery + spec = importlib.machinery.PathFinder.find_spec(name, path) + if spec and spec.origin and any(spec.origin.startswith(r) for r in self.roots): + with open(spec.origin, encoding="utf-8") as f: + if "REJECT" in f.read(): + gate_errors.append(name) + raise ImportGateError(f"typedpython: {name} does not type-check") + return None + + def install(roots): + finder = Finder(list(roots)) + sys.meta_path.insert(0, finder) + self.installed.append(finder) + + def uninstall(): + for finder in self.installed: + if finder in sys.meta_path: + sys.meta_path.remove(finder) + + package = types.ModuleType("typedpython") + importgate = types.ModuleType("typedpython.importgate") + importgate.install = install + importgate.uninstall = uninstall + importgate.ImportGateError = ImportGateError + package.importgate = importgate + sys.modules["typedpython"] = package + sys.modules["typedpython.importgate"] = importgate + self.gate_errors = gate_errors + + self.overlay = tempfile.mkdtemp(prefix="devagent-gated-") + self.agent = devagent.start(port=0, token=TOKEN, overlay=self.overlay) + self.client = Client(self.agent.port) + self.client.request("hello", token=TOKEN) + + def tearDown(self): + self.client.close() + self.agent.stop() + sys.modules["typedpython.importgate"].uninstall() + for name in ["typedpython", "typedpython.importgate", "devagent_fixture_gated"]: + sys.modules.pop(name, None) + if self.overlay in sys.path: + sys.path.remove(self.overlay) + + def test_the_gate_is_installed_for_the_overlay(self): + self.assertEqual([[self.overlay]], [f.roots for f in self.installed]) + + def test_a_reload_the_gate_rejects_keeps_the_old_module(self): + self.client.request("push", files={"devagent_fixture_gated.py": "VALUE = 1\n"}) + self.client.request("execute", code="import devagent_fixture_gated") + done = self.client.request("push", files={"devagent_fixture_gated.py": "VALUE = 2 # REJECT\n"})[-1] + self.assertEqual("error", done["status"]) + self.assertEqual("ImportGateError", done["ename"]) + self.assertIn("does not type-check", done["evalue"]) + self.assertEqual(["devagent_fixture_gated"], self.gate_errors) + replies = self.client.request("execute", code="devagent_fixture_gated.VALUE") + self.assertEqual("1", [r for r in replies if r["type"] == "result"][0]["data"]["text/plain"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/python-multiplatform/src/wasmJsMain/README.md b/python-multiplatform/src/wasmJsMain/README.md index f3d136f40..360880eb5 100644 --- a/python-multiplatform/src/wasmJsMain/README.md +++ b/python-multiplatform/src/wasmJsMain/README.md @@ -1,11 +1,11 @@ -# wasmJsMain — rules +# wasmJsMain, rules Kotlin/Wasm reaching CPython 3.14 built for `wasm32-emscripten`, through `@WasmImport` against a **shared linear memory**. Not a JavaScript bridge: there is no JS frame in either the call path or the data path. Everything below is measured. The standalone `wasm-experiment/` that first took the measurements -was retired into this source set and `tools/wasm/` (it remains in git history: `git log -- +was retired into this source set and `python-multiplatform/scripts/wasm/` (it remains in git history: `git log -- wasm-experiment`); `docs/platforms/wasm-design.md` holds the current decisions and numbers, and `docs/archive/wasm-design-experiment-log.md` records how they were arrived at, including the several conclusions that were wrong before they were run. Everything the experiment proved that this file @@ -17,7 +17,7 @@ stands on is now a test in `wasmJsTest/.../emscripten/`, run against the real in | the shared memory survives CPython growing it | `WasmSharedMemoryGrowthTest` | | Kotlin-only work neither grows nor writes that memory | `WasmKotlinLeavesLinearMemoryAloneTest` | | the interpreter carries the `pyemscripten_2026_0` ABI | `WasmInterpreterAbiTest` | -| ... and a real compiled wheel loads and runs in it | `WasmCompiledWheelTest` (wheels: `tools/wasm/build-cpython.sh wheels`) | +| ... and a real compiled wheel loads and runs in it | `WasmCompiledWheelTest` (wheels: `python-multiplatform/scripts/wasm/build-cpython.sh wheels`) | | direct calls beat a JS frame; hoisting beats a shim; strings and bulk reads | `WasmCrossingOverheadTest` | | `Table.set` beats `addFunction` + a JS closure for upcalls | `WasmUpcallRouteOverheadTest` | | `PY_CALL_TRAMPOLINE`'s JS fallback is live; `pmp_invoke` survives the wasm one | `WasmCallTrampolineTest` | @@ -25,7 +25,7 @@ stands on is now a test in `wasmJsTest/.../emscripten/`, run against the real in The measurement tests print their rows and assert only orderings that held by a wide margin; the figures quoted below are the experiment's, and the tests' own output is the current reading. -`tools/wasm/build-cpython.sh stock` builds the unpatched PEP 776 interpreter (plus only the runtime +`python-multiplatform/scripts/wasm/build-cpython.sh stock` builds the unpatched PEP 776 interpreter (plus only the runtime methods the library binds through) as the negative control for the ABI tests: against it, `WasmInterpreterAbiTest` and `WasmCompiledWheelTest` must fail. @@ -42,7 +42,7 @@ It is the only allocator Kotlin/Wasm offers, and it assumes it owns the whole me grew the memory to two pages and handed back `0x0`. On the memory we share with Emscripten that is the null page and the static data region, so the first use corrupts CPython's heap. -This is this target's equivalent of desktop's "always `invokeExact`" — the one rule that is not a +This is this target's equivalent of desktop's "always `invokeExact`", the one rule that is not a preference. `Pointer(addr)` is the way in. Its constructor is public and `loadByte`/`storeByte`/`loadInt` @@ -56,7 +56,7 @@ Emscripten factory. **Do not remove those two lines**, and do not restore the pr lifetime of the process. Emscripten decides whether to use JavaScript Promise Integration by **runtime feature detection**, -not at build time — CPython's link line has no `-sJSPI`. When it detects JSPI it turns the blocking +not at build time, CPython's link line has no `-sJSPI`. When it detects JSPI it turns the blocking syscalls into suspending imports, and it installs the matching `WebAssembly.promising` wrapper on exactly one export: `main`. @@ -74,17 +74,17 @@ cause, and the wasm suite dies as *"process exited unexpectedly"* rather than as It cost this repo two sections of `docs/design/upcall.md` (§9.5, §14.4) to misdiagnose as "wasm cannot do `asyncio`". It could not do `asyncio` because `selectors.py` calls -`select.poll().poll(0)` at import time to pick its `DefaultSelector` — which is also why `import +`select.poll().poll(0)` at import time to pick its `DefaultSelector`, which is also why `import select` was fine and `import selectors` was not. §15 has the measurement. Measured, same `python.wasm`, `select.poll().poll(0)`: Node 22 (no JSPI) returns `[]`; Node 26 (JSPI on by default, and the Gradle runner's version) raises `SuspendError`. There is no V8 flag to turn -JSPI off on Node 26 — it is shipped, not experimental — so suppressing the feature detection is the +JSPI off on Node 26, it is shipped, not experimental, so suppressing the feature detection is the only lever. The synchronous path is the one this library wants anyway: every call arrives from Kotlin as a plain synchronous wasm call, so a blocking syscall has to actually block. **The known cost, recorded rather than hidden: this mutates a host intrinsic globally.** Under the -Node test runner that is contained — nothing else in the process wants JSPI, and the 344-test wasm +Node test runner that is contained, nothing else in the process wants JSPI, and the 344-test wasm suite is the evidence. On a browser page shared with *another* wasm module that uses JSPI, deleting these would break that module. The honest fix for such a page is an Emscripten build that does not feature-detect, not a narrower delete, because `__maybe_poll_async` re-reads @@ -98,11 +98,11 @@ a browser wasm target and it runs (ROADMAP §10). Same browser, Chromium 150: | a page that does not load the app | `function` | | the app's page, 20 s after load | `undefined` | -So the delete is real rather than a no-op — this browser ships JSPI and the property is gone +So the delete is real rather than a no-op, this browser ships JSPI and the property is gone page-wide. Nothing on that page broke: Compose came up (`canvases=1`), skiko rendered, Kotlin/Wasm ran. CPython, Kotlin/Wasm and skiko are three wasm modules on one page and **none of the other two uses JSPI**, which is the whole reason it is survivable. The warning is unchanged in force; what has -changed is that its scope is now observed — it costs nothing until a page loads a JSPI consumer. +changed is that its scope is now observed, it costs nothing until a page loads a JSPI consumer. ## The memory is shared because Kotlin *imports* it @@ -111,13 +111,13 @@ defining one. 2.4.10 and earlier exported one instead, which could not be made t Emscripten memory import and a Kotlin `@WasmImport` need each other's instantiation to come first. A wasm memory import accepts any memory whose limits sit inside its own, and an unbounded import -accepts everything — so `intrinsics.memory` takes Emscripten's `Module.wasmMemory` unchanged. +accepts everything, so `intrinsics.memory` takes Emscripten's `Module.wasmMemory` unchanged. **The whole data-path integration is that one substitution**, done in `build.gradle.kts` against the generated `*.import-object.mjs`. Emscripten is built exactly as it would be alone: no `-sIMPORTED_MEMORY`, no binary patching. (There is now a second substitution, on the generated *entry* module, and it exists for upcalls -rather than for the memory — see "Upcalls work" below. This section used to say "the whole +rather than for the memory, see "Upcalls work" below. This section used to say "the whole integration" without qualification, and that stopped being true when `ProxyTypeFactory` was wired up.) @@ -132,17 +132,17 @@ underneath Kotlin. A JS `TypedArray` view detaches when that happens; a wasm mem ## `Py_ssize_t` is 32-bit here, and it is the only ABI divergence `EmbedAPI.kt` types `Py_ssize_t` as Kotlin `Long`, which is right on every other target. On wasm32 -it is `i32`. **Fourteen functions are affected** — `PyList_New/Size/GetItem/SetItem/Insert`, +it is `i32`. **Fourteen functions are affected**, `PyList_New/Size/GetItem/SetItem/Insert`, `PyTuple_New/Size/GetItem/GetSlice/SetItem`, `PyDict_Size`, `PySet_Size`, `PyObject_Size`, -`PyObject_Length` — and `EmbedAPI.wasmJs.kt` converts at the boundary through two named functions +`PyObject_Length`, and `EmbedAPI.wasmJs.kt` converts at the boundary through two named functions rather than inline casts, because the two directions fail in opposite ways: - `Int.pySsizeToLong()` **sign-extends**. `-1` is the error return of nearly every function in the group, and the unsigned widening that is *correct* for pointers here would turn it into - 4294967295 — a value that passes `if (n < 0)` and is then used as a length. + 4294967295, a value that passes `if (n < 0)` and is then used as a length. - `Long.toPySsize()` **range-checks instead of truncating**. `PyList_New(0x1_0000_0000)` would otherwise become `PyList_New(0)` and hand back an empty list: a wrong answer rather than a - failure. Nothing outside `Int` can be legitimate — wasm32's whole address space is 4 GiB. + failure. Nothing outside `Int` can be legitimate, wasm32's whole address space is 4 GiB. ### Two guards, because the two halves fail differently @@ -161,7 +161,7 @@ breaking one declaration produces: This is the same shape as `generateDesktopReachabilityMetadata`: derived from the artefact that decides, on every run, never from a checked-in copy of the answer. It also asserts that every -`external fun` in the file was parsed — an earlier version required an explicit return type and so +`external fun` in the file was parsed, an earlier version required an explicit return type and so silently skipped the three `Unit`-returning declarations, which is precisely the "green test measuring its own scope" failure ROADMAP §2 records. @@ -187,7 +187,7 @@ Measured, both routes ending with a Kotlin `String` built from the same C addres | `UTF8ToString` through JS | 125 | 3 827 | `decodeToString()` costs about 13 ns/byte on long input against 0.2 ns/byte on short. `Wasm` -therefore scans once — deciding length and ASCII-ness together — and decodes into a `CharArray` +therefore scans once, deciding length and ASCII-ness together, and decodes into a `CharArray` either way. **Do not assume a JS crossing costs a string copy.** Kotlin/Wasm compiles with @@ -212,14 +212,14 @@ against the real interpreter, reading one global 200,000 times: | one crossing, direct `@WasmImport` | **2.9** | 75% comes off through pure-Kotlin optimisation. A composed shim could merge the two remaining calls -and save 2.9 ns. Same verdict ROADMAP §6 reached for desktop, with a wider margin — and it removes +and save 2.9 ns. Same verdict ROADMAP §6 reached for desktop, with a wider margin, and it removes a second build pipeline. ## String arguments: two routes, and the reason each is cheap is *not* the reason on Android Every `actual` taking a `String` used to `malloc`, encode, copy and `free` per call. All 59 argument positions across 49 functions now go through one of two routes, chosen per argument the -way `desktopMain` and `androidMain` choose — and, checked against them, with the identical verdict +way `desktopMain` and `androidMain` choose, and, checked against them, with the identical verdict on all 59: Wasm.internedUtf8 repeated identifiers: attribute, method, module, type names, dict keys @@ -233,8 +233,8 @@ iterations, `WasmMarshallingOverheadTest`: | `malloc` + `encodeToByteArray` + copy + `free` (what this target used to do) | **82.7** | | | `malloc` + `free` alone | 13.0 | | | `encodeToByteArray` alone | 59.2 | | -| `Wasm.scratchUtf8` — reused buffer, direct write | **30.6** | **157.0** | -| `Wasm.internedUtf8` — cache hit | **22.2** | **26.4** | +| `Wasm.scratchUtf8`, reused buffer, direct write | **30.6** | **157.0** | +| `Wasm.internedUtf8`, cache hit | **22.2** | **26.4** | And end to end, every row back to back in one run so that only the marshalling differs: @@ -251,15 +251,15 @@ Three things in there, two of them against what the other platforms would predic - **The expensive part was the intermediate `ByteArray`, not the encode.** `encodeToByteArray()` plus a copy is 59.2 ns; writing the same bytes straight into linear memory is what makes scratch 30.6 ns. A WasmGC array allocation per C API call was most of the cost, and shared memory is what - lets it go — the destination is CPython's own heap, so there is no staging buffer. That is the + lets it go, the destination is CPython's own heap, so there is no staging buffer. That is the *opposite* of the original design note's claim that shared memory removes a string copy. It removes the allocation. - **Interning's value is entirely in the string length, and the crossover is high.** For a - 7-character name it is worth 8 ns over scratch — nothing like Android's 15x (2238 → 148 ns). + 7-character name it is worth 8 ns over scratch, nothing like Android's 15x (2238 → 148 ns). For a 54-character one it is worth **6x** (157.0 → 26.4), because a cache hit is flat in the length and an encode is linear in it. Both are true, and reporting only the short case would have argued interning is barely worth having. -- **The end-to-end gap is smaller than the standalone gap**, because the rest of +- **The end-to-end gap is smaller than the standalone gap**: because the rest of `PyObject_GetAttrString` is CPython's own work. 258.0 → 229.2 ns through the `actual`. One negative result, recorded because the obvious reading of it is wrong: `BenchmarkTest`'s shared @@ -269,7 +269,7 @@ per-call difference of ~29 ns; the in-run A/B rows above are the measurement, an figure is not usable for a change this size in either direction. **Do not read `scratchUtf8`'s address as durable.** Four slots, rotating; the fifth call overwrites -the first. Four is chosen against the surface — the most any function here stages at once is three +the first. Four is chosen against the surface, the most any function here stages at once is three (`PyErr_WarnExplicit`, `PyImport_ExecCodeModuleWithPathnames`). A re-entrant call that marshals four more strings while these are live would overwrite them, exactly as on desktop. `allocUtf8`/`freeUtf8` remains for buffers the caller owns, and `ProxyTypeFactory`'s type name is one. @@ -282,12 +282,12 @@ exports. What the build *does* need is `wasmExports,wasmMemory` added to `-sEXPORTED_RUNTIME_METHODS`; without them the exports are in the binary but unreachable from JS, so there is nothing to hand `@WasmImport`. Neither setting is in PEP 783's ABI-sensitive list. -## Reclamation is automatic, through JS — and it is the only `js(…)` in this source set +## Reclamation is automatic, through JS, and it is the only `js(…)` in this source set The Kotlin/Wasm **stdlib** has no finalisation hook. That much was always true, and was checked directly against `kotlin-stdlib-wasm-js-2.4.20-Beta2.klib`: no `FinalizationRegistry`, no `WeakRef`, -no `Cleaner`. The conclusion drawn from it — that a `PyObject` dropped without `close()` must leak -here — was wrong. The *host* has all three, and `registerCleaner` now reaches them. +no `Cleaner`. The conclusion drawn from it, that a `PyObject` dropped without `close()` must leak +here, was wrong. The *host* has all three, and `registerCleaner` now reaches them. `PyAutoCloseable.wasmJs.kt` hands the cleaner to a JS `FinalizationRegistry` as a `JsReference`, with a small `CleanupState` (pointer, decref action, done flag) as the held value. The weakly @@ -314,7 +314,7 @@ after one macrotask WeakRef CLEARED registry callbacks 200 A `WeakRef` keeps its target alive for the job that created it, and the registry callback is delivered as a *task*. `FinalizationRegistry.prototype.cleanupSome()`, which would have made it -synchronous, has been removed from V8 — `--harmony-weak-refs-with-cleanup-some` is rejected as an +synchronous, has been removed from V8, `--harmony-weak-refs-with-cleanup-some` is rejected as an unrecognised flag, and `setFlagsFromString` reports the same. **So there is no synchronous drain on this platform, at all.** `commonTest`'s `collectorTest` exists for that: on every other target it is a blocking loop, and here it is a chain of host turns. `GCLeakTest`'s three cases pass on this @@ -326,15 +326,15 @@ flag to the wasmJs test task's `nodeJsArgs`. Where it is absent, `forceGC()` deg the bounded loop gives up rather than pretending. **This file is the only `js(…)` in `wasmJsMain`, and it has to be.** Everything else here is -`@WasmImport`, which is a wasm-to-wasm call with no JavaScript frame — but `@WasmImport` carries +`@WasmImport`, which is a wasm-to-wasm call with no JavaScript frame, but `@WasmImport` carries primitives only, and what has to cross here is a *reference* to the Kotlin object whose reachability is the whole question. There is no handle-table trick that avoids it. The cost is confined to construction and destruction; nothing on the C API call path goes through it: | ns / op | | |---|---| -| `registerCleaner` + `close()` — the whole hook | **88.6** | -| `toJsReference()` alone — the externref crossing | 44.1 | +| `registerCleaner` + `close()`, the whole hook | **88.6** | +| `toJsReference()` alone, the externref crossing | 44.1 | (50 000 iterations each, after an equal warm-up. Reading the *first* row recorded without a warm-up gave 300 ns and made the second look three times cheaper than a superset of itself.) @@ -346,7 +346,7 @@ route and `outstanding` is a leak count rather than a live-object count. One property this target has that the threaded ones do not: **the callback cannot arrive inside a Python call.** JS tasks run only once the stack has unwound and every call into CPython here is -synchronous, so the decref never re-enters the interpreter from within another call — the hazard +synchronous, so the decref never re-enters the interpreter from within another call, the hazard ROADMAP §1 spent a section on. ## Upcalls work, and cycles are collected @@ -358,8 +358,8 @@ ROADMAP §1 spent a section on. leave the heap type's refcount exactly where it started. The call path has no JavaScript in it. `@WasmExport` produces a raw wasm export; -`WebAssembly.Table.prototype.set` accepts it into CPython's own `__indirect_function_table` — a -funcref is a funcref whatever instance produced it — and the index **is** the C function pointer. +`WebAssembly.Table.prototype.set` accepts it into CPython's own `__indirect_function_table`, a +funcref is a funcref whatever instance produced it, and the index **is** the C function pointer. CPython reaches Kotlin through `call_indirect` at 3.1 ns, against 10.9 ns for the `addFunction`-plus-JS-closure route the original design specified. @@ -368,22 +368,22 @@ CPython reaches Kotlin through `call_indirect` at 3.1 ns, against 10.9 ns for th **1. The executable module must declare the trampolines. The library cannot.** `@WasmExport` is honoured only in the compilation that produces the `.wasm`. Measured, not inferred: the identical annotation on the identical function exports from `wasmJsTest` and does not from -`wasmJsMain`, whose klib is linked in — the test binary's export section came out holding +`wasmJsMain`, whose klib is linked in, the test binary's export section came out holding `startUnitTests` and nothing else. So an application has to write three delegating lines itself; see `ProxyTypeExportNames`, and `wasmJsTest/.../ProxyTypeExports.kt`, which is that file. Having the Gradle plugin generate it is the obvious next step and is not done. **2. Registration is a JS startup step, so the build patches the entry module.** A table index cannot be obtained from inside Kotlin, and `WebAssembly.Table.set` needs the funcref -out of `wasmInstance.exports`. `cpython.mjs` cannot fetch that itself — it is imported *by* Kotlin's +out of `wasmInstance.exports`. `cpython.mjs` cannot fetch that itself, it is imported *by* Kotlin's import object, so importing the entry module back would be an ES cycle across a top-level await. The `doFirst` in `build.gradle.kts` therefore appends `pmpSetKotlinExports(exports)` to the generated entry module. That is a **second** substitution; this file used to say the integration was one. **3. Kotlin/Wasm has no `call_indirect`, so calling a C function *pointer* goes through JS.** `tp_traverse` is handed a `visitproc` and `tp_dealloc` has to reach its type's `tp_free`; both are -table indices. `pmpCallVisit`/`pmpCallFree` in `cpython.mjs` do `table.get(i)(...)`. Both are cold — -inside a cyclic collection, or a proxy's deallocation — and neither is on the downcall path or on +table indices. `pmpCallVisit`/`pmpCallFree` in `cpython.mjs` do `table.get(i)(...)`. Both are cold, +inside a cyclic collection, or a proxy's deallocation, and neither is on the downcall path or on the path CPython uses to *enter* Kotlin. **`call_indirect` is statically typed and does not coerce.** A `(i32) -> i32` export called through a @@ -393,8 +393,8 @@ signatures are written out in `ProxyType`'s documentation next to the C ones. Prefer slots CPython invokes directly (`PyType_FromSpec`) over `PyMethodDef` entries: the latter go through `_PyEM_TrampolineCall`, whose wasm fast path never installs on this toolchain pair (an -upstream defect — the `EM_JS` initialiser needs `wasmTable`/`wasmMemory` before they exist and the -`LinkError` is swallowed by a bare `catch`). The cost is symmetric, though — every C extension pays +upstream defect, the `EM_JS` initialiser needs `wasmTable`/`wasmMemory` before they exist and the +`LinkError` is swallowed by a bare `catch`). The cost is symmetric, though, every C extension pays the same JS frame, measured at 14 ns over `abs()`. `wasmJsTest/.../WasmCallTrampolineTest` pins both halves of this. It registers a four-parameter @@ -412,31 +412,31 @@ Kotlin as a bare `JsException` with nothing in it. `cpython.mjs` reads C strings ## A browser bundle needs four things this library must hand it -Node and a browser differ in exactly one place — the **filesystem** — and in three build steps. All +Node and a browser differ in exactly one place, the **filesystem**, and in three build steps. All four are the library's to supply; ROADMAP §10 has the reasoning for why none of them is the consumer's problem to discover. `stageWasmBrowserRuntime` in `build.gradle.kts` is the task. 1. **`cpython.mjs` must be in the consumer's webpack context.** The *generated import object* of any module that links this library carries `import * as ... from './cpython.mjs'`, and nothing - propagates the file — it is not in the wasmJs klib's resources either. Missing, webpack fails with + propagates the file, it is not in the wasmJs klib's resources either. Missing, webpack fails with `Module not found: Error: Can't resolve './cpython.mjs'` and that is the whole error. 2. **`python.mjs` and `python.wasm` must sit next to the page.** `cpython.mjs` loads the glue with - `import(/* webpackIgnore: true */ "./python.mjs")` — dynamic so that `node:fs` is never resolved + `import(/* webpackIgnore: true */ "./python.mjs")`, dynamic so that `node:fs` is never resolved in a web bundle, and webpack-ignored because Emscripten's 567 KB of glue branches on `require`, `node:fs` and `import.meta.url` at runtime and must not be bundled. 3. **The standard library travels as `python3..zip`.** There is no NODEFS in a browser. `cpython.mjs` fetches the zip into MEMFS at `/lib/python3.zip` during `preRun`, under `addRunDependency`, and sets **no `thisProgram`** so that `sys.prefix` stays `/`. That is what CPython's own `Tools/wasm/emscripten/web_example/python.worker.mjs` does. -4. **Two substitutions on the compile-sync output**, between the sync and webpack: +4. **Two substitutions on the compile-sync output**: between the sync and webpack: `intrinsics.memory` → Emscripten's `wasmMemory`, and `pmpSetKotlinExports(exports)` into the - generated entry module. **The second goes *before* `exports._start()`, not appended after it** — + generated entry module. **The second goes *before* `exports._start()`, not appended after it**, `_start()` is Kotlin `main()`, and an appended handoff runs after the application has finished. Appending was correct for this module's own test bundle, whose entry module has no `_start()` call, which is why it survived until an executable existed. **That last sentence is also why no test guards it.** `wasmJsBrowserTest` runs the *test* bundle, - and reverting the placement produces a byte-identical file there — checked, and all ten browser + and reverting the placement produces a byte-identical file there, checked, and all ten browser tests stayed green. `patchKotlinWasmOutputForCPython` therefore asserts the ordering as a postcondition and refuses to write the file; `:sample`'s copy does the same, and that is the only build in the repository whose bundle can trip it. @@ -452,11 +452,11 @@ webpack, the absence of a filesystem, or a host intrinsic. 4.5 s warm (16 s if t has to be recompiled and re-bundled). It needs a Chromium-family browser and **skips with a message** when there is none, the same way a missing interpreter does; `CHROME_BIN` overrides the probe list in `build.gradle.kts`. `karma.config.d/cpython.js` serves the interpreter to -karma — `webpackCopy` for the glue (the bundle's directory is a fresh temp path every run) and a +karma, `webpackCopy` for the glue (the bundle's directory is a fresh temp path every run) and a proxy for the document-relative stdlib zip. Needs a CPython Emscripten build. The default location is `/.caches/wasm-runtime`, which -`./gradlew :python-multiplatform:buildWasmPython` (that is, `tools/wasm/build-cpython.sh`) fills; +`./gradlew :python-multiplatform:buildWasmPython` (that is, `python-multiplatform/scripts/wasm/build-cpython.sh`) fills; `-PwasmPythonDir=` or `PMP_PYTHON_DIR` override it. The task **skips with a message** rather than failing when it is absent. The script matches `pyemscripten_2026_0` (PEP 783) closely enough to load a compiled PyPI wheel, and its `verify` step checks the stdlib zip as well as `python.wasm` -- the @@ -465,10 +465,10 @@ interpreter reported `PYEMSCRIPTEN_PLATFORM_VERSION = None`. **An unpacked `python-multiplatform-wasm-runtime` zip works just as well, and that is what CI is wired to use.** What the tests need is not the CPython build tree but the five files -`stageWasmBrowserRuntime` stages out of it — `python.wasm`, `python.mjs`, the stdlib zip and the two +`stageWasmBrowserRuntime` stages out of it, `python.wasm`, `python.mjs`, the stdlib zip and the two glue modules. That was not true until the standard library stopped arriving through NODEFS: under Node, `sys.path[0]` was `/lib/python314.zip`, a file nothing had ever created, and the stdlib was -really coming from entry 1 — the CPython **source checkout** beside the build directory, which no +really coming from entry 1, the CPython **source checkout** beside the build directory, which no artefact carries. `cpython.mjs` now installs the staged zip into MEMFS on both hosts, so unzip python-multiplatform-wasm-runtime-.zip -d /tmp/rt @@ -476,23 +476,23 @@ artefact carries. `cpython.mjs` now installs the staged zip into MEMFS on both h runs the whole suite (344/0/0, measured) against nothing but a published artefact. -`-PrequireWasmRuntime=true` turns every one of those skips — the interpreter, and for the browser task -the absence of a Chromium-family browser — into a failure naming what was missing. Pass it whenever +`-PrequireWasmRuntime=true` turns every one of those skips, the interpreter, and for the browser task +the absence of a Chromium-family browser, into a failure naming what was missing. Pass it whenever a *caller* asked for the suite, so that a skip cannot be reported to them as success over zero tests. `.github/workflows/wasm.yml` passes it; without it that workflow reported green having run nothing, and did for months. `verifyWasmAbiSignatures` runs first, automatically, and skips the same way. Run it alone when -`bindings.kt` changes — it is a second or two and it turns "0 tests ran, `LinkError`" into a line +`bindings.kt` changes, it is a second or two and it turns "0 tests ran, `LinkError`" into a line naming the function. Current state, re-measured rather than carried over from the paragraph above: **403 tests, 0 failed, 0 skipped** (`wasmJsNodeTest`), including the three `GCLeakTest` cases and the four -`WasmFinalizationTest` cases — see "Reclamation is automatic, through JS" above for how those pass. +`WasmFinalizationTest` cases, see "Reclamation is automatic, through JS" above for how those pass. The "214 tests, 3 failed" this line used to read was the state before that section's fix landed and had drifted out of step with the "344/0/0" already recorded a few paragraphs up; both are stale now -that the suite has grown further, which is expected — re-run `wasmJsNodeTest` rather than trusting +that the suite has grown further, which is expected, re-run `wasmJsNodeTest` rather than trusting either number. `emsdk` is pinned to **5.0.3**, which is what `pyemscripten_2026_0` specifies. Installing another -version replaces `~/emsdk/upstream` **in place** — it is a global setting, not per-project. +version replaces `~/emsdk/upstream` **in place**, it is a global setting, not per-project. diff --git a/python-multiplatform/src/wasmJsMain/kotlin/python/multiplatform/env/PythonPayload.wasmJs.kt b/python-multiplatform/src/wasmJsMain/kotlin/python/multiplatform/env/PythonPayload.wasmJs.kt index e43ade2d6..04f98d087 100644 --- a/python-multiplatform/src/wasmJsMain/kotlin/python/multiplatform/env/PythonPayload.wasmJs.kt +++ b/python-multiplatform/src/wasmJsMain/kotlin/python/multiplatform/env/PythonPayload.wasmJs.kt @@ -6,8 +6,8 @@ package python.multiplatform.env * The standard library reaches this target by being preloaded into Emscripten's virtual filesystem * at *build* time (see `wasmJsMain/README.md` and `cpython.mjs`), which is also the only way a * consumer's Python could get here: the payload would have to be baked into that same image by the - * bundling step, not discovered at run time. Nothing in `toolchain` produces a wasm bundle today — - * `PythonStagingPlatform` has three destinations and wasm is not one of them — so returning an + * bundling step, not discovered at run time. Nothing in `toolchain` produces a wasm bundle today, + * `PythonStagingPlatform` has three destinations and wasm is not one of them, so returning an * empty list is the honest answer rather than a stub awaiting a directory that no build writes. */ internal actual fun discoverStagedPayloadRoots(): List = emptyList() diff --git a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt index b90848d93..b53f2f7d5 100644 --- a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt +++ b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/TestWheelConfig.kt @@ -4,7 +4,7 @@ package python.native.ffi.emscripten /** * Where the `KotlinJsTest` `doFirst` in `python-multiplatform/build.gradle.kts` extracted the - * pinned wheels `tools/wasm/build-cpython.sh wheels` downloads, or `null` when none were found. + * pinned wheels `python-multiplatform/scripts/wasm/build-cpython.sh wheels` downloads, or `null` when none were found. * Test-only: appended to the test bundle's `cpython-config.mjs`, never to a consumer's. */ external val PMP_TEST_SITE_PACKAGES: JsString? diff --git a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt index 1c76fe96e..bb4da7064 100644 --- a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt +++ b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmCompiledWheelTest.kt @@ -16,12 +16,12 @@ import python.multiplatform.ffi.PythonTestFixture * PyPI) is `dlopen`ed as an Emscripten side module, links against `python.wasm`, and runs. * * Measured in the former `wasm-experiment/` (Test G) through CPython's own `python.sh`, with no - * Kotlin in the process. Against a stock PEP 776 build (`tools/wasm/build-cpython.sh stock`) the same + * Kotlin in the process. Against a stock PEP 776 build (`python-multiplatform/scripts/wasm/build-cpython.sh stock`) the same * import fails at load: * * LinkError: Import #204 "env" "__cpp_exception": tag import requires a WebAssembly.Tag * - * The wheels are not in the repository. `tools/wasm/build-cpython.sh wheels` downloads them by URL + * The wheels are not in the repository. `python-multiplatform/scripts/wasm/build-cpython.sh wheels` downloads them by URL * with pinned sha256 into `.caches/wasm-wheels`, and the test task extracts them next to the test * bundle. Without them this test skips with that instruction, and under `-PrequireWasmRuntime=true` * it fails instead -- the same contract every other wasm prerequisite has. @@ -39,7 +39,7 @@ class WasmCompiledWheelTest { val site = PMP_TEST_SITE_PACKAGES?.toString() if (site == null) { val message = "no pyemscripten_2026_0 wheels found in ${PMP_TEST_WHEELS_DIR?.toString()}. " + - "Run tools/wasm/build-cpython.sh wheels -- it downloads the pinned pydantic-core and " + + "Run python-multiplatform/scripts/wasm/build-cpython.sh wheels -- it downloads the pinned pydantic-core and " + "typing-extensions wheels there -- and re-run this task." if (required.toBoolean()) { fail("$message -PrequireWasmRuntime=true was passed, so this is a failure rather than a skip.") diff --git a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt index d7a26057a..5c800f878 100644 --- a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt +++ b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmInterpreterAbiTest.kt @@ -7,7 +7,7 @@ import python.multiplatform.ffi.Python3 import python.multiplatform.ffi.PythonTestFixture /** - * The interpreter under test is the one `tools/wasm/build-cpython.sh` produces: CPython matched to + * The interpreter under test is the one `python-multiplatform/scripts/wasm/build-cpython.sh` produces: CPython matched to * PEP 783's `pyemscripten_2026_0`, so that a compiled PyPI wheel with that tag can load. * * A stock PEP 776 build differs in exactly the two places asserted here, and both fail silently diff --git a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmSharedMemoryGrowthTest.kt b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmSharedMemoryGrowthTest.kt index d7a3c039c..a0202cf2a 100644 --- a/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmSharedMemoryGrowthTest.kt +++ b/python-multiplatform/src/wasmJsTest/kotlin/python/native/ffi/emscripten/WasmSharedMemoryGrowthTest.kt @@ -17,7 +17,7 @@ import python.native.ffi.Wasm * CPython links `-sALLOW_MEMORY_GROWTH -sINITIAL_MEMORY=20971520`, so growth is not hypothetical: * it happens the first time anything allocates past the initial 20 MiB. A JS `TypedArray` view * detaches when the buffer grows; Kotlin holds the memory as a wasm *import* and does not. The - * whole data path of this target — every `char*` read and every C string Kotlin writes — rests on + * whole data path of this target, every `char*` read and every C string Kotlin writes, rests on * that, and until this test it was proved only by the standalone `wasm-experiment/` (Tests C and * D, now in git history), which no build ran. * diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 510d18a76..c80d43103 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -34,7 +34,7 @@ plugins { // Gradle 8.11.1, which AGP 8.10 requires), so the split is gone and this module holds its own // Python-facing declarations again -- `src/*/kotlin/.../demo/bindings/`. // - // `ksp-fixtures/android` is the regression test. It is the first fixture to apply an Android + // `python-multiplatform-ksp/fixtures/android` is the regression test. It is the first fixture to apply an Android // plugin, which is exactly why nothing caught this. id("io.github.thisisthepy.python.multiplatform.bindings") } @@ -217,7 +217,7 @@ fun patchWasmOutputForCPython(dir: File, modulePrefix: String) { // first browser run found. `_start()` is Kotlin `main()`; the library's own test bundle has // no such call (its runner starts the tests from outside), so appending had always been // enough and every executable bundle would have registered its upcall entry point one line - // too late. The symptom is `pmpRegisterUpcall returned -1` — `kotlinExports` still null — + // too late. The symptom is `pmpRegisterUpcall returned -1`, `kotlinExports` still null, // for an application whose sections 1-4 all worked. val startCall = "exports._start();" val note = "// Added by :sample's build: upcall registration needs the raw wasm exports,\n" + diff --git a/sample/python/pyproject.toml b/sample/pyproject.toml similarity index 100% rename from sample/python/pyproject.toml rename to sample/pyproject.toml diff --git a/sample/src/androidMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainActivity.kt b/sample/src/androidMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainActivity.kt index bcbf3db63..80a09fc23 100644 --- a/sample/src/androidMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainActivity.kt +++ b/sample/src/androidMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainActivity.kt @@ -15,8 +15,8 @@ private const val TAG = "PythonDemo" * `Py_Initialize`: CPython's stdlib ships inside the library module's assets and has to be * unpacked to a readable directory, and `PYTHONHOME` has to name that directory's prefix. * - * That was ~55 lines here — an ABI lookup, a version-derived stdlib path, a recursive - * `AssetManager` copy, and an "already unpacked?" probe — all of it duplicated in the external + * That was ~55 lines here, an ABI lookup, a version-derived stdlib path, a recursive + * `AssetManager` copy, and an "already unpacked?" probe, all of it duplicated in the external * consumer app and in the library's own instrumentation fixture, and all of it wrong about the * same thing (a copy interrupted partway through looked complete to the probe). It is * [PythonBootstrap] now, shipped in the AAR, and the host app's share is one call. diff --git a/sample/src/commonMain/kotlin/org/thisisthepy/python/multiplatform/demo/ui/App.kt b/sample/src/commonMain/kotlin/org/thisisthepy/python/multiplatform/demo/ui/App.kt index a920e8a48..6f356abc2 100644 --- a/sample/src/commonMain/kotlin/org/thisisthepy/python/multiplatform/demo/ui/App.kt +++ b/sample/src/commonMain/kotlin/org/thisisthepy/python/multiplatform/demo/ui/App.kt @@ -71,7 +71,7 @@ fun App() { /** 1. The interpreter is really up, and it is the build's configured one. */ @Composable private fun RuntimeSection() { - DemoCard("1 — embedded interpreter") { + DemoCard("1, embedded interpreter") { val summary = remember { runCatching { PythonDemo.runtimeSummary() }.getOrElse { "not started: ${it.message}" } } Mono(summary) } @@ -80,7 +80,7 @@ private fun RuntimeSection() { /** 2. Kotlin builds a Python object, Python computes with it, Kotlin reads the result back. */ @Composable private fun EvaluateSection() { - DemoCard("2 — object model round trip") { + DemoCard("2, object model round trip") { var expression by remember { mutableStateOf(PythonDemo.DEFAULT_EXPRESSION) } var result by remember { mutableStateOf("") } @@ -99,7 +99,7 @@ private fun EvaluateSection() { /** 3. The direction that had no example at all until now: Python calling Kotlin. */ @Composable private fun UpcallSection() { - DemoCard("3 — Python calls Kotlin (upcall)") { + DemoCard("3, Python calls Kotlin (upcall)") { var presses by remember { mutableStateOf(0) } var answer by remember { mutableStateOf("") } @@ -121,7 +121,7 @@ private fun UpcallSection() { /** 4. Proof the KSP processor ran, read off the runtime table rather than off the build log. */ @Composable private fun TableSection() { - DemoCard("4 — generated function table") { + DemoCard("4, generated function table") { Mono(UpcallDemo.tableSummary()) if (UpcallDemo.available) { Mono( @@ -141,7 +141,7 @@ private fun TableSection() { */ @Composable private fun ClassProxySection() { - DemoCard("5 — Python drives a Kotlin class") { + DemoCard("5, Python drives a Kotlin class") { var result by remember { mutableStateOf("") } Mono(remember { PythonDemo.proxyInstallReport() }) @@ -154,7 +154,7 @@ private fun ClassProxySection() { /** 6. The companion object, reached through the class and never through an instance. */ @Composable private fun StaticSurfaceSection() { - DemoCard("6 — companion members on the metaclass") { + DemoCard("6, companion members on the metaclass") { var result by remember { mutableStateOf("") } Text("Greeter.built = 100; Greeter.forget(); Greeter.PUNCTUATION = '?'") @@ -172,7 +172,7 @@ private fun StaticSurfaceSection() { */ @Composable private fun AwaitSection() { - DemoCard("7 — await over a suspend fun") { + DemoCard("7, await over a suspend fun") { // Neither button below may run on wasmJs: PythonDemo.awaitFastPath() and // PythonDemo.awaitSuspending() both reach `import asyncio`, which traps this wasm // instance rather than raising (docs/design/upcall.md §5.8) -- there would be no @@ -186,7 +186,7 @@ private fun AwaitSection() { var fast by remember { mutableStateOf("") } var slow by remember { mutableStateOf("") } - Text("await g.greetNow(1) and await g.greetLater(2) — same call site, two paths underneath.") + Text("await g.greetNow(1) and await g.greetLater(2), same call site, two paths underneath.") Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { Button(onClick = { fast = PythonDemo.awaitFastPath() }) { Text("fast path") } Spacer(Modifier.width(4.dp)) diff --git a/sample/python/src/main/example_py/__init__.py b/sample/src/commonMain/python/example_py/__init__.py similarity index 100% rename from sample/python/src/main/example_py/__init__.py rename to sample/src/commonMain/python/example_py/__init__.py diff --git a/sample/python/src/main/example_py/data/table.json b/sample/src/commonMain/python/example_py/data/table.json similarity index 100% rename from sample/python/src/main/example_py/data/table.json rename to sample/src/commonMain/python/example_py/data/table.json diff --git a/sample/src/iosMain/app-Info.plist b/sample/src/iosMain/app-Info.plist index 4d292fb22..aaf5e182d 100644 --- a/sample/src/iosMain/app-Info.plist +++ b/sample/src/iosMain/app-Info.plist @@ -29,8 +29,6 @@ UILaunchScreen - UILaunchStoryboardName - Launch Screen.storyboard UIRequiredDeviceCapabilities armv7 diff --git a/sample/src/iosMain/app.xcodeproj/project.pbxproj b/sample/src/iosMain/app.xcodeproj/project.pbxproj index 841cb271a..4117bea77 100644 --- a/sample/src/iosMain/app.xcodeproj/project.pbxproj +++ b/sample/src/iosMain/app.xcodeproj/project.pbxproj @@ -7,7 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 6E386A4C481B4D329CCF934C /* python in Resources */ = {isa = PBXBuildFile; fileRef = F91AA643B9094B8D8ED04A9C /* python */; }; + 8B3F1A512CC49F1D00B85330 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B3F1A502CC49F1D00B85330 /* Python.xcframework */; }; + 8B3F1A5C2CC574B200B85330 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B3F1A502CC49F1D00B85330 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8B1A047D2B85BEFC0008D451 /* Preview Content in Resources */ = {isa = PBXBuildFile; fileRef = 8B1A047C2B85BEFC0008D451 /* Preview Content */; }; 8B1A04802B85BF3E0008D451 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B1A047F2B85BF3E0008D451 /* Assets.xcassets */; }; 8B1A04982B85C3820008D451 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1A04972B85C3820008D451 /* iOSApp.swift */; }; @@ -31,9 +32,6 @@ 8B1A04CB2B85CD050008D451 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B1A04CA2B85CD050008D451 /* QuartzCore.framework */; }; 8B1A04CD2B85CD100008D451 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B1A04CC2B85CD100008D451 /* UIKit.framework */; }; 8B1A04CF2B85CD190008D451 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B1A04CE2B85CD190008D451 /* WebKit.framework */; }; - 8B1A04DD2B85CEE90008D451 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B1A04DC2B85CEE90008D451 /* Launch Screen.storyboard */; }; - 8BFA8DAD2B8B4C9C00E220D3 /* include in Resources */ = {isa = PBXBuildFile; fileRef = 8BFA8DAC2B8B4C9B00E220D3 /* include */; }; - 8BFA8DAF2B8B4CAF00E220D3 /* lib in Resources */ = {isa = PBXBuildFile; fileRef = 8BFA8DAE2B8B4CAF00E220D3 /* lib */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -43,6 +41,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 8B3F1A5C2CC574B200B85330 /* Python.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -50,8 +49,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - F91AA643B9094B8D8ED04A9C /* python */ = {isa = PBXFileReference; lastKnownFileType = folder; name = python; path = "../../build/pythonStaging/ios/python"; sourceTree = ""; }; - 7555FF7B242A565900829871 /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8B3F1A502CC49F1D00B85330 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Python.xcframework; path = "../../build/xcode-frameworks/Python.xcframework"; sourceTree = ""; }; + 7555FF7B242A565900829871 /* PythonDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PythonDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8B1A04782B85BED60008D451 /* app-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "app-Info.plist"; sourceTree = ""; }; 8B1A047C2B85BEFC0008D451 /* Preview Content */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "Preview Content"; path = "swift/Preview Content"; sourceTree = ""; }; 8B1A047F2B85BF3E0008D451 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = swift/Assets.xcassets; sourceTree = ""; }; @@ -81,9 +80,6 @@ 8B1A04D12B85CD360008D451 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; 8B1A04D22B85CD4A0008D451 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 8B1A04D32B85CD580008D451 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; - 8B1A04DC2B85CEE90008D451 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = "Launch Screen.storyboard"; path = "swift/Storyboards/Launch Screen.storyboard"; sourceTree = ""; }; - 8BFA8DAC2B8B4C9B00E220D3 /* include */ = {isa = PBXFileReference; lastKnownFileType = folder; name = include; path = ../../dist/toolchain/darwin/root/python3/include; sourceTree = ""; }; - 8BFA8DAE2B8B4CAF00E220D3 /* lib */ = {isa = PBXFileReference; lastKnownFileType = folder; name = lib; path = ../../dist/toolchain/darwin/root/python3/lib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -91,6 +87,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8B3F1A512CC49F1D00B85330 /* Python.xcframework in Frameworks */, 8B1A04CB2B85CD050008D451 /* QuartzCore.framework in Frameworks */, 8B1A04B72B85CC930008D451 /* CoreMedia.framework in Frameworks */, 8B1A04B32B85CC7F0008D451 /* CoreGraphics.framework in Frameworks */, @@ -128,7 +125,7 @@ 7555FF7C242A565900829871 /* Products */ = { isa = PBXGroup; children = ( - 7555FF7B242A565900829871 /* app.app */, + 7555FF7B242A565900829871 /* PythonDemo.app */, ); name = Products; sourceTree = ""; @@ -136,6 +133,7 @@ 7555FFB0242A642200829871 /* Frameworks */ = { isa = PBXGroup; children = ( + 8B3F1A502CC49F1D00B85330 /* Python.xcframework */, 8B1A04D32B85CD580008D451 /* libz.tbd */, 8B1A04D22B85CD4A0008D451 /* libsqlite3.tbd */, 8B1A04D12B85CD360008D451 /* libc++.tbd */, @@ -166,9 +164,7 @@ isa = PBXGroup; children = ( 8B1A047F2B85BF3E0008D451 /* Assets.xcassets */, - 8B1A047E2B85BF0B0008D451 /* EmbeddedPython */, 8B1A047C2B85BEFC0008D451 /* Preview Content */, - 8B1A04DB2B85CECC0008D451 /* Storyboards */, 8B1A04782B85BED60008D451 /* app-Info.plist */, ); name = Resources; @@ -184,15 +180,6 @@ name = Sources; sourceTree = ""; }; - 8B1A047E2B85BF0B0008D451 /* EmbeddedPython */ = { - isa = PBXGroup; - children = ( - 8BFA8DAC2B8B4C9B00E220D3 /* include */, - 8BFA8DAE2B8B4CAF00E220D3 /* lib */, - ); - name = EmbeddedPython; - sourceTree = ""; - }; 8B1A04A62B85C5480008D451 /* Bridges */ = { isa = PBXGroup; children = ( @@ -202,14 +189,6 @@ name = Bridges; sourceTree = ""; }; - 8B1A04DB2B85CECC0008D451 /* Storyboards */ = { - isa = PBXGroup; - children = ( - 8B1A04DC2B85CEE90008D451 /* Launch Screen.storyboard */, - ); - name = Storyboards; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -217,10 +196,11 @@ isa = PBXNativeTarget; buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "app" */; buildPhases = ( - 7555FFB5242A651A00829871 /* ShellScript */, + 7555FFB5242A651A00829871 /* Compile Kotlin Framework */, 7555FF77242A565900829871 /* Sources */, 7555FF78242A565900829871 /* Frameworks */, 7555FF79242A565900829871 /* Resources */, + 8B3F1A532CC49F9C00B85330 /* Install Python Standard Library and Payload */, 7555FFB4242A642300829871 /* Embed Frameworks */, ); buildRules = ( @@ -228,8 +208,8 @@ dependencies = ( ); name = app; - productName = iosApp; - productReference = 7555FF7B242A565900829871 /* app.app */; + productName = app; + productReference = 7555FF7B242A565900829871 /* PythonDemo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -272,19 +252,34 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6E386A4C481B4D329CCF934C /* python in Resources */, - 8BFA8DAF2B8B4CAF00E220D3 /* lib in Resources */, - 8BFA8DAD2B8B4C9C00E220D3 /* include in Resources */, 8B1A047D2B85BEFC0008D451 /* Preview Content in Resources */, 8B1A04802B85BF3E0008D451 /* Assets.xcassets in Resources */, - 8B1A04DD2B85CEE90008D451 /* Launch Screen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 7555FFB5242A651A00829871 /* ShellScript */ = { + 7555FFB5242A651A00829871 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/../../..\"\n./gradlew :python-multiplatform:embedAndSignAppleFrameworkForXcode\n./gradlew :sample:embedAndSignAppleFrameworkForXcode\n"; + }; + 8B3F1A532CC49F9C00B85330 /* Install Python Standard Library and Payload */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -294,13 +289,14 @@ ); inputPaths = ( ); + name = "Install Python Standard Library and Payload"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/../../..\"\n./gradlew :sample:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "set -e\n# SPEC L-10 / issue #59: the CPython stdlib and this app's python/ payload into the .app, with\n# extension modules wrapped as frameworks. See docs/platforms/ios-app-bundle.md.\ncd \"$SRCROOT/../../..\"\nexport PYTHON_PAYLOAD_DIR=\"$SRCROOT/../commonMain/python\"\n/bin/bash python-multiplatform-gradle-plugin/src/main/resources/xcode/install-python.sh\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -369,7 +365,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -424,7 +421,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -441,15 +439,17 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = NO; CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ZP73KJVT6Q; DEVELOPMENT_ASSET_PATHS = "swift/Preview\\ Content"; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(SRCROOT)/../../../build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + "$(SRCROOT)/../../build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); INFOPLIST_FILE = "app-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -459,8 +459,8 @@ "-framework", Demo, ); - PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = org.thisisthepy.python.multiplatform.demo; + PRODUCT_NAME = PythonDemo; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -474,15 +474,17 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = NO; CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ZP73KJVT6Q; DEVELOPMENT_ASSET_PATHS = "swift/Preview\\ Content"; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(SRCROOT)/../../../build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + "$(SRCROOT)/../../build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); INFOPLIST_FILE = "app-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -492,8 +494,8 @@ "-framework", Demo, ); - PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = org.thisisthepy.python.multiplatform.demo; + PRODUCT_NAME = PythonDemo; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainViewController.kt b/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainViewController.kt index df65bc651..2f159a115 100644 --- a/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainViewController.kt +++ b/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/MainViewController.kt @@ -10,7 +10,7 @@ import org.thisisthepy.python.multiplatform.demo.ui.App * from inside a composable body -- on every recomposition, and only once the user had expanded * the section that hosted it. * - * On iOS the shipped framework carries no stdlib. An app built through `iosApp/`'s "Install Python + * On iOS the shipped framework carries no stdlib. An app built through `sample/src/iosMain/app.xcodeproj`'s "Install Python * Standard Library and Payload" phase carries one in its bundle and the library finds it there * (SPEC L-10); see `python-multiplatform/src/iosMain/README.md`. */ diff --git a/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/bindings/UpcallDemo.ios.kt b/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/bindings/UpcallDemo.ios.kt index cbb0aeaaa..324c95226 100644 --- a/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/bindings/UpcallDemo.ios.kt +++ b/sample/src/iosMain/kotlin/org/thisisthepy/python/multiplatform/demo/bindings/UpcallDemo.ios.kt @@ -17,7 +17,7 @@ import python.multiplatform.reflection.UpcallTable * `@InstallsUpcallTable expect fun installGeneratedUpcallTable()` and every leaf gets its own * one-liner generated into it. The constraint is unchanged -- the generated table is reachable * only from the source set of the target that generated it -- but nothing shared code writes has - * to know that any more. `ksp-fixtures/app`'s `androidNativeMain` pins the same shape. + * to know that any more. `python-multiplatform-ksp/fixtures/app`'s `androidNativeMain` pins the same shape. */ /** diff --git a/sample/src/iosMain/swift/Assets.xcassets/AppIcon.appiconset/Contents.json b/sample/src/iosMain/swift/Assets.xcassets/AppIcon.appiconset/Contents.json index fb88a396b..8edf56e7a 100644 --- a/sample/src/iosMain/swift/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/sample/src/iosMain/swift/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,93 +1,9 @@ { "images" : [ { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", + "filename" : "app-icon-1024.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], @@ -95,4 +11,4 @@ "author" : "xcode", "version" : 1 } -} \ No newline at end of file +} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/sample/src/iosMain/swift/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png similarity index 100% rename from iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png rename to sample/src/iosMain/swift/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png diff --git a/sample/src/iosMain/swift/ContentView.swift b/sample/src/iosMain/swift/ContentView.swift index 55013484b..3653dcb62 100644 --- a/sample/src/iosMain/swift/ContentView.swift +++ b/sample/src/iosMain/swift/ContentView.swift @@ -1,4 +1,19 @@ -import Foundation -import ComposeApp +import UIKit +import SwiftUI +import Demo -let mainViewController = Main_iosKt.MainViewController() + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + MainViewControllerKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ContentView: View { + var body: some View { + ComposeView() + .ignoresSafeArea(.keyboard) // Compose has own keyboard handler + } +} diff --git a/sample/src/iosMain/swift/Storyboards/Launch Screen.storyboard b/sample/src/iosMain/swift/Storyboards/Launch Screen.storyboard deleted file mode 100644 index 7765358e9..000000000 --- a/sample/src/iosMain/swift/Storyboards/Launch Screen.storyboard +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sample/src/iosMain/swift/iOSApp.swift b/sample/src/iosMain/swift/iOSApp.swift index 0c61aab1f..d83dca611 100644 --- a/sample/src/iosMain/swift/iOSApp.swift +++ b/sample/src/iosMain/swift/iOSApp.swift @@ -1,13 +1,10 @@ -import UIKit +import SwiftUI -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = mainViewController - window?.makeKeyAndVisible() - return true +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } } -} +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4e19f85ff..fadc453a2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,26 +44,49 @@ include(":python-multiplatform-ksp") // The Compose host: the `@Composable` that draws a Python-declared root. Its own module so that // `:python-multiplatform` never depends on Compose. include(":python-multiplatform-compose") -include(":ksp-fixtures:library") -include(":ksp-fixtures:app") +// Fixtures live inside the module whose output they check (issue #108), as nested Gradle projects. +// The KSP processor's consumers are `python-multiplatform-ksp/fixtures/*`. The intermediate +// `:python-multiplatform-ksp:fixtures` project that Gradle creates for the path has no build file +// and no plugins; it is only a container. +include(":python-multiplatform-ksp:fixtures:library") +include(":python-multiplatform-ksp:fixtures:app") // The fixture that carries an Android plugin. `:library` and `:app` apply none, which is why the // KSP/AGP minimum-version wall of ROADMAP §13 stayed invisible to them while it broke every // Android consumer of the bindings plugin. -include(":ksp-fixtures:android") +include(":python-multiplatform-ksp:fixtures:android") + +// The Gradle plugin is an *included build* (see `pluginManagement` above), so it cannot own +// subprojects of this build. Its fixtures are therefore root-build projects whose directory is +// `python-multiplatform-gradle-plugin/fixtures/` (the plugin build's own settings never +// look there). The project path is `:python-multiplatform-gradle-plugin-fixtures:`: a path +// `:python-multiplatform-gradle-plugin:...` would share its name with the included build. // The fixture for the *other* binding producer: `docs/design/ecosystem.md` §5b's artefact walker, which // reads the jars the build resolves rather than the source it compiles. `:library` and `:app` cover // KSP only, so nothing here exercised a third-party binary until this module existed. -include(":ksp-fixtures:artifact") -// The composable half of the same claim, and the one module in this build that applies the Compose -// compiler plugin for a *test*: `:ksp-fixtures:artifact` deliberately has none (its own -// `build.gradle.kts` says why), and a `Composer` cannot exist without one. See its -// `PythonComposition.kt` -- one hand-written `@Composable`, for every composable in every artefact. -include(":ksp-fixtures:compose") +include(":python-multiplatform-gradle-plugin-fixtures:artifact") // A real, separately-compiled jar for the walker's value-class handling -- `kotlin.time.Duration` // cannot prove a positive round trip (its constructor is `internal`), so this stands in for it. See // its own `build.gradle.kts`. -include(":ksp-fixtures:artifact-valueclass") +include(":python-multiplatform-gradle-plugin-fixtures:artifact-valueclass") // The same walker, pointed at a Kotlin/Native compile classpath instead of a JVM one -- // ROADMAP §16e's "investigated, not implemented" half. `KlibScanner` reads `.klib` metadata with // `LibraryAbiReader` instead of ASM. -include(":ksp-fixtures:klib-artifact") +include(":python-multiplatform-gradle-plugin-fixtures:klib-artifact") +// The container project for the three above (no build file, no plugins), then each fixture. +project(":python-multiplatform-gradle-plugin-fixtures").projectDir = + file("python-multiplatform-gradle-plugin/fixtures") +for (name in listOf("artifact", "artifact-valueclass", "klib-artifact")) { + project(":python-multiplatform-gradle-plugin-fixtures:$name").projectDir = + file("python-multiplatform-gradle-plugin/fixtures/$name") +} + +// The Compose host's consumers: `python-multiplatform-compose/fixtures/*`. +// The composable half of the same claim, and the one module in this build that applies the Compose +// compiler plugin for a *test*: `:python-multiplatform-gradle-plugin-fixtures:artifact` deliberately +// has none (its own `build.gradle.kts` says why), and a `Composer` cannot exist without one. See its +// `PythonComposition.kt` -- one hand-written `@Composable`, for every composable in every artefact. +include(":python-multiplatform-compose:fixtures:compose") +// pythonx-compose's `UI.ipynb` scenarios end to end (issue #26): the installed pythonx-compose wheel, +// drawn by `PythonContent`. A fixture and not `sample/`, which is user-authored spec material. Its +// desktopTest needs `-PpythonxComposeWheel=`; see its README. +include(":python-multiplatform-compose:fixtures:notebook-e2e") diff --git a/tools/loop-verify.sh b/tools/loop-verify.sh deleted file mode 100755 index bc1eaf31c..000000000 --- a/tools/loop-verify.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Verification gate for the paseo loop. Exits 0 only if every suite that the loop -# may have touched is green. Counts come from the XML, not from exit codes, and the -# results directories are cleared first so a crashed run cannot report stale numbers. -# -# tools/loop-verify.sh [checkout] defaults to the checkout this script lives in -# -# Each suite is its own Gradle invocation: one invocation can hide an ordering dependency -# between modules (it once hid 24 failures in :ksp-fixtures:artifact). -set -u -ROOT="${1:-$(cd "$(dirname "$0")/.." && pwd)}" -cd "$ROOT" || exit 1 -export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" -LOGDIR="$ROOT/.tmp/loop-verify" -mkdir -p "$LOGDIR" - -rm -rf python-multiplatform/build/test-results \ - ksp-fixtures/app/build/test-results \ - ksp-fixtures/compose/build/test-results \ - ksp-fixtures/artifact/build/test-results \ - python-multiplatform-gradle-plugin/build/test-results - -rc=0 -for task in :python-multiplatform:desktopTest :ksp-fixtures:app:desktopTest \ - :ksp-fixtures:compose:desktopTest :ksp-fixtures:artifact:desktopTest \ - :python-multiplatform-gradle-plugin:test \ - :python-multiplatform:compileKotlinAndroidNativeArm64; do - log="$LOGDIR/$(echo "$task" | tr ':' '_').log" - ./gradlew "$task" --rerun --console=plain > "$log" 2>&1 - r=$? - echo "$task exit=$r compile-errors=$(grep -c '^e: ' "$log")" - [ "$r" -eq 0 ] || rc=1 -done - -python3 - <<'PY' -import glob, sys, xml.etree.ElementTree as ET -bad = 0 -for label, pat in [ - ("desktop", "python-multiplatform/build/test-results/desktopTest/*.xml"), - ("app", "ksp-fixtures/app/build/test-results/desktopTest/*.xml"), - ("compose", "ksp-fixtures/compose/build/test-results/desktopTest/*.xml"), - ("artifact", "ksp-fixtures/artifact/build/test-results/desktopTest/*.xml"), - ("plugin", "python-multiplatform-gradle-plugin/build/test-results/test/*.xml"), -]: - t = f = e = 0 - for p in glob.glob(pat): - r = ET.parse(p).getroot() - t += int(r.get('tests')); f += int(r.get('failures')); e += int(r.get('errors')) - print(f"{label}: {t}/{f}/{e}") - if t == 0 or f or e: - bad = 1 -sys.exit(bad) -PY -py=$? - -[ "$rc" -eq 0 ] && [ "$py" -eq 0 ]