From f5beb25bb43691a650185502f960584470359139 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 24 Oct 2026 22:06:59 +0200 Subject: [PATCH] Support PHP 8.2 in the Turbo extension The shared core built against PHP 8.5 headers loads on PHP 8.2 next to a thin 8.2 extension; the shared-core gate reports identical shared code for 8.2 through 8.6. What 8.2 lacks or spells differently is routed through pt_abi or pinned in abi.h: - no engine C stack limit: Abi.cpp computes the bounds like 8.3 does and keeps them across fresh-stack coroutines - zend_fcall_info_cache.closure: 8.3's layout is declared on top of 8.2's - the literal-name arginfo bit is translated to the plain name bit - zend_zval_value_name() routes to zend_zval_type_name() on 8.2 - codegen pins for zend_string_*, smart_str_extract, ZEND_HASH_FILL_* and ZEND_ASSUME CI builds and tests 8.2 everywhere 8.3 is built; Alpine 3.24 has no php82 package, so the musl legs build it from the tarball. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0156KuUPxcWUY7jap6uxnA5h --- .github/scripts/install-alpine-php.sh | 22 +- .github/turbo-build/Dockerfile | 6 +- .github/turbo-build/build-php.sh | 12 +- .github/workflows/phar.yml | 47 ++-- .github/workflows/turbo-build-image.yml | 8 +- src/Turbo/TurboExtensionSelector.php | 2 +- turbo-ext/README.md | 8 +- turbo-ext/bin/shared-core/check-linux.sh | 2 +- .../bin/shared-core/fingerprint-linux.sh | 6 +- turbo-ext/bin/shared-core/gate-linux.sh | 2 +- turbo-ext/bin/shared-core/link-split-linux.sh | 6 +- turbo-ext/composer.json | 2 +- turbo-ext/src/Abi.cpp | 211 +++++++++++++++++- turbo-ext/src/Engine.h | 5 +- turbo-ext/src/GenericObjectType.cpp | 4 - turbo-ext/src/abi.h | 155 ++++++++++++- turbo-ext/src/reg.h | 14 +- turbo-ext/src/support.h | 15 ++ turbo-ext/tests/type-family.php | 17 +- turbo-ext/tests/walk-trace.php | 10 +- 20 files changed, 480 insertions(+), 74 deletions(-) diff --git a/.github/scripts/install-alpine-php.sh b/.github/scripts/install-alpine-php.sh index 9ab49c6086e..6cb58f71844 100644 --- a/.github/scripts/install-alpine-php.sh +++ b/.github/scripts/install-alpine-php.sh @@ -1,24 +1,26 @@ #!/bin/sh # Installs PHP $PHP_MINOR and the extension's build tools into the Alpine -# containers of the musl turbo legs in phar.yml. With PHP_ZTS=1 it builds a -# thread-safe PHP from the release tarball instead (build-php.sh): Alpine -# packages no thread-safe PHP, while the official php:*-alpine Docker images -# are thread-safe from 8.6 on. +# containers of the musl turbo legs in phar.yml. It builds PHP from the +# release tarball instead (build-php.sh) with PHP_ZTS=1 — Alpine packages +# no thread-safe PHP, while the official php:*-alpine Docker images are +# thread-safe from 8.6 on — and for a minor the Alpine release no longer +# packages (8.2). set -eu apk add --no-cache bash curl git make g++ musl-dev linux-headers patch tar zstd -if [ "${PHP_ZTS:-0}" = "1" ]; then - apk add --no-cache jq pkgconf xz libxml2-dev oniguruma-dev curl-dev openssl-dev zlib-dev - PHP_MINOR="$PHP_MINOR" PHP_ZTS=1 sh "$(dirname "$0")/../turbo-build/build-php.sh" - exit 0 -fi - V="$(echo "$PHP_MINOR" | tr -d .)" # Alpine's php86 packages are currently only in edge/testing. if [ "$PHP_MINOR" = "8.6" ]; then echo 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories fi + +if [ "${PHP_ZTS:-0}" = "1" ] || [ -z "$(apk search -q -x "php$V")" ]; then + apk add --no-cache jq pkgconf xz libxml2-dev oniguruma-dev curl-dev openssl-dev zlib-dev + PHP_MINOR="$PHP_MINOR" PHP_ZTS="${PHP_ZTS:-0}" sh "$(dirname "$0")/../turbo-build/build-php.sh" + exit 0 +fi + apk add --no-cache \ "php$V" "php$V-dev" "php$V-ctype" "php$V-curl" "php$V-mbstring" \ "php$V-tokenizer" "php$V-iconv" "php$V-openssl" "php$V-phar" \ diff --git a/.github/turbo-build/Dockerfile b/.github/turbo-build/Dockerfile index 1b3a143e76f..e81fefac3e3 100644 --- a/.github/turbo-build/Dockerfile +++ b/.github/turbo-build/Dockerfile @@ -1,7 +1,7 @@ # Prebuilt image for the turbo-compile gnu legs in phar.yml, published to -# ghcr.io/phpstan/turbo-build by turbo-build-image.yml (tags gnu-php8.3, -# gnu-php8.4, gnu-php8.5, gnu-php8.6 and the thread-safe gnu-php8.3-zts to -# gnu-php8.6-zts; each a linux/amd64 + linux/arm64 manifest). +# ghcr.io/phpstan/turbo-build by turbo-build-image.yml (tags gnu-php8.2 to +# gnu-php8.6 and the thread-safe gnu-php8.2-zts to gnu-php8.6-zts; each a +# linux/amd64 + linux/arm64 manifest). # # Baking the PHP toolchain in keeps apt and the ondrej/php PPA out of the # compile jobs entirely: ports.ubuntu.com (the only Ubuntu arm64 mirror, diff --git a/.github/turbo-build/build-php.sh b/.github/turbo-build/build-php.sh index 567962bde83..2cb50b12348 100644 --- a/.github/turbo-build/build-php.sh +++ b/.github/turbo-build/build-php.sh @@ -60,7 +60,14 @@ tar -xJf php.tar.xz -C php-src --strip-components=1 rm php.tar.xz cd php-src -# shellcheck disable=SC2086 # ZTS_FLAG is empty or a single word +# PHP 8.2's x86-64 JIT does not assemble for musl ZTS with binutils 2.43+ +# (alpine:3.24): "@TLSGD operator requires `%rdi' as dest register", fixed +# only from 8.3. Nothing these builds run uses the JIT. +JIT_FLAG="" +case "$PHP_VERSION" in + 8.2.*) JIT_FLAG="--disable-opcache-jit" ;; +esac +# shellcheck disable=SC2086 # ZTS_FLAG and JIT_FLAG are empty or a single word ./configure \ --prefix=/usr/local \ --disable-cgi \ @@ -72,7 +79,8 @@ cd php-src --with-curl \ --with-openssl \ --with-zlib \ - $ZTS_FLAG + $ZTS_FLAG \ + $JIT_FLAG make -j"$(nproc)" make install diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index c49e61e65f9..e54e2617428 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -188,8 +188,8 @@ jobs: with: egress-policy: audit - # the compiler-tests job runs the phar on PHP 8.2, where no binary exists - # and the note is not printed + # the compiler-tests job runs the phar only for `list`, which prints + # no note - name: "Install PHP" uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0 with: @@ -828,7 +828,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] + php-version: ["8.2", "8.3", "8.4", "8.5", "8.6"] # the musl arm64 legs are in turbo-compile-musl-arm64 target: - name: "linux-gnu-x86_64" @@ -1002,7 +1002,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] + php-version: ["8.2", "8.3", "8.4", "8.5", "8.6"] # thread-safe for every minor too, see turbo-compile ts: ["nts", "zts"] # see turbo-compile @@ -1125,7 +1125,7 @@ jobs: fail-fast: false matrix: # every PHP the cores run on; thread-safe builds are "-zts" - php-version: ["8.3", "8.4", "8.5", "8.6", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] + php-version: ["8.2", "8.3", "8.4", "8.5", "8.6", "8.2-zts", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] steps: - name: Harden the runner (Audit all outbound calls) @@ -1257,6 +1257,7 @@ jobs: fail-fast: false matrix: image: + - "php:8.2-cli-bookworm" - "php:8.3.21-cli-bookworm" - "php:8.3.22-cli-bookworm" - "php:8.4-cli-bookworm" @@ -1267,6 +1268,9 @@ jobs: - "php:8.6-rc-cli-alpine" check: ["tests", "phpstan"] include: + - image: "php:8.2-cli-bookworm" + php-version: "8.2" + binary: "linux-gnu-x86_64-php8.2" - image: "php:8.3.21-cli-bookworm" php-version: "8.3" binary: "linux-gnu-x86_64-php8.3" @@ -1562,16 +1566,19 @@ jobs: strategy: fail-fast: false matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] + php-version: ["8.2", "8.3", "8.4", "8.5", "8.6"] ts: ["nts", "zts"] # see turbo-compile variant: ${{ fromJSON((startsWith(github.ref, 'refs/tags/') || contains(github.event.pull_request.labels.*.name, 'turbo-pie')) && '["split", "pie"]' || '["split"]') }} - # The official 8.3 Windows binaries are built with VS16 (linker - # 14.2x) and PHP's module loader is one-directional (module toolset - # generation <= core's), so the 8.3 DLL must be linked with the v142 - # toolset — which the windows-2022 image ships alongside the default - # v143. The devel pack name carries the same vs infix. + # The official 8.2 and 8.3 Windows binaries are built with VS16 + # (linker 14.2x) and PHP's module loader is one-directional (module + # toolset generation <= core's), so their DLLs must be linked with + # the v142 toolset — which the windows-2022 image ships alongside the + # default v143. The devel pack name carries the same vs infix. include: + - php-version: "8.2" + vs: "vs16" + toolset: "14.29" - php-version: "8.3" vs: "vs16" toolset: "14.29" @@ -1851,7 +1858,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ["8.3", "8.4", "8.5", "8.6"] + php-version: ["8.2", "8.3", "8.4", "8.5", "8.6"] target: ["linux-gnu-x86_64", "linux-gnu-arm64", "macos-arm64", "windows-x86_64"] ts: ["nts", "zts"] # macOS ships no ZTS binary (see turbo-compile) @@ -1878,6 +1885,16 @@ jobs: container: "ghcr.io/phpstan/turbo-build:gnu-php8.6" # the linux-gnu ZTS legs, in the image the binary is compiled in # (see above); the official php:*-zts images get turbo-docker-run + - target: "linux-gnu-x86_64" + php-version: "8.2" + ts: "zts" + runs-on: "ubuntu-latest" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.2-zts" + - target: "linux-gnu-arm64" + php-version: "8.2" + ts: "zts" + runs-on: "ubuntu-24.04-arm" + container: "ghcr.io/phpstan/turbo-build:gnu-php8.2-zts" - target: "linux-gnu-x86_64" php-version: "8.3" ts: "zts" @@ -2049,7 +2066,7 @@ jobs: fail-fast: false matrix: arch: ["x86_64", "arm64"] - php-version: ["8.3", "8.4", "8.5", "8.6"] + php-version: ["8.2", "8.3", "8.4", "8.5", "8.6"] # thread-safe for every minor too, see turbo-compile ts: ["nts", "zts"] @@ -2148,7 +2165,7 @@ jobs: # vs17-built official PHP loads it fine. matrix: operating-system: ["ubuntu-latest", "windows-latest"] - php-version: ["8.3", "8.4", "8.5", "8.6"] + php-version: ["8.2", "8.3", "8.4", "8.5", "8.6"] ts: ["nts", "zts"] script: ["make tests", "make phpstan"] # The Linux ZTS binaries run the same checks in turbo-docker-run, in @@ -2346,7 +2363,7 @@ jobs: include: # below TurboExtensionSelector::MINIMUM_PHP_VERSION_ID there is no # binary to find: the workers get the OPcache entries alone - - php-version: "8.2" + - php-version: "8.1" turbo: "off" - php-version: "8.5" turbo: "on" diff --git a/.github/workflows/turbo-build-image.yml b/.github/workflows/turbo-build-image.yml index 066dcb0cc9c..e0999da933d 100644 --- a/.github/workflows/turbo-build-image.yml +++ b/.github/workflows/turbo-build-image.yml @@ -3,8 +3,8 @@ name: "Build Turbo compile images" # Builds .github/turbo-build/Dockerfile natively on x86_64 and arm64 -# runners and publishes ghcr.io/phpstan/turbo-build:gnu-php{8.3,8.4,8.5,8.6} -# and the thread-safe gnu-php{8.3,8.4,8.5,8.6}-zts as multi-arch manifests. The +# runners and publishes ghcr.io/phpstan/turbo-build:gnu-php{8.2,8.3,8.4,8.5,8.6} +# and the thread-safe gnu-php{8.2,8.3,8.4,8.5,8.6}-zts as multi-arch manifests. The # turbo-compile gnu legs in phar.yml run in these images, so their only # network dependency is a GHCR pull — no apt, no ports.ubuntu.com, no PPA at # compile time. @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: # [-zts]; a -zts tag builds a thread-safe PHP of that minor - tag: ["8.3", "8.4", "8.5", "8.6", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] + tag: ["8.2", "8.3", "8.4", "8.5", "8.6", "8.2-zts", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] platform: - arch: "amd64" runs-on: "ubuntu-latest" @@ -106,7 +106,7 @@ jobs: strategy: matrix: - tag: ["8.3", "8.4", "8.5", "8.6", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] + tag: ["8.2", "8.3", "8.4", "8.5", "8.6", "8.2-zts", "8.3-zts", "8.4-zts", "8.5-zts", "8.6-zts"] env: TAG: "ghcr.io/phpstan/turbo-build:gnu-php${{ matrix.tag }}" diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index 0137256b7ed..c19c6bc9241 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -56,7 +56,7 @@ final class TurboExtensionSelector * matrix. Older runtimes have no binary to find, so they skip the lookup * (and with it the process restart) entirely. */ - public const MINIMUM_PHP_VERSION_ID = 80300; + public const MINIMUM_PHP_VERSION_ID = 80200; /** @api */ public const EXTENSION_PATH_INI = 'phpstan.turboExtensionPath'; diff --git a/turbo-ext/README.md b/turbo-ext/README.md index ea55ddd0b21..1d8569c67be 100644 --- a/turbo-ext/README.md +++ b/turbo-ext/README.md @@ -14,7 +14,7 @@ self-analysis). **Most users do not need to install anything.** The [phpstan/phpstan](https://github.com/phpstan/phpstan) Composer package ships prebuilt binaries for the most common platforms — Linux (glibc and musl, -x86_64 and arm64), macOS (arm64), and Windows (x86_64), for PHP 8.3 and newer — and +x86_64 and arm64), macOS (arm64), and Windows (x86_64), for PHP 8.2 and newer — and PHPStan automatically loads the one matching your runtime into its worker processes. @@ -420,6 +420,10 @@ layout moved through `abi.h`: | `ZSTR_KNOWN()` ids, `IS_REFERENCE_EX`, `ZEND_ACC_USE_GUARDS`, lazy-object flags | `pt_abi` values (`PT_ABI_KNOWN_STRINGS`, …) | | ZPP error codes / `Z_EXPECTED_*` numbering, parse slow paths (8.6) | neutral ZPP macros in `abi.h`, translated by `Abi.cpp` | | functions whose signature or behaviour changed (`zend_create_closure`, `zend_dval_to_lval`, …) or that stopped being exported (`zend_call_known_function`, …) | function pointers in `pt_abi`, implemented in `Abi.cpp` | +| `EG(stack_base)` / `EG(stack_limit)` and `zend_call_stack.h` (8.3) | `pt_abi.eg_stack_*`; before 8.3, `Abi.cpp`'s own pair, determined like 8.3 does, and the two inline helpers spelled out in `abi.h` | +| `zend_fcall_info_cache.closure` (8.3 appended it) | 8.3's layout declared over the older headers' struct (`support.h`, `abi.h`); the older engines use the first four members, the shared code never reads `closure` | +| class names of internal arginfo (`_ZEND_TYPE_LITERAL_NAME_BIT`, 8.3) | pinned in `abi.h`, turned into the plain name bit by `Abi.cpp` before 8.3 | +| `zend_zval_value_name()` (8.3; 8.2's type errors use `zend_zval_type_name()`) | `pt_abi.zval_value_name` | | `#if PHP_VERSION_ID` gates | `PT_ABI_SINCE(version, then, otherwise)`, decided at run time | | helpers re-spelled without changing behaviour (macro → inline function) | one definition pinned in `abi.h` ("codegen pins") | @@ -459,7 +463,7 @@ against them (the import library expected in `turbo-ext/`). In CI (`.github/workflows/phar.yml`), `turbo-compile-core*` build one core per platform — profile-guided, against `php-version` of its matrix (Windows: -against 8.3 with VS16, the oldest toolset, since PHP checks the toolset of +against 8.3 with VS16, the oldest toolset — 8.2's too — since PHP checks the toolset of the DLL it loads, the extension, and a core linked with an older one runs on any newer VC runtime) — and are what `turbo-origins` reuses from earlier runs; `turbo-compile*` link each version's extension against them in a diff --git a/turbo-ext/bin/shared-core/check-linux.sh b/turbo-ext/bin/shared-core/check-linux.sh index 07e5f2dcd9e..4f319c7c510 100755 --- a/turbo-ext/bin/shared-core/check-linux.sh +++ b/turbo-ext/bin/shared-core/check-linux.sh @@ -21,7 +21,7 @@ set -euo pipefail S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} HERE=$(cd "$(dirname "$0")" && pwd) REFERENCE=${GATE_REFERENCE:-8.5} -VERSIONS=${GATE_VERSIONS:-8.3 8.4 8.5 8.6 8.3-zts 8.4-zts 8.5-zts 8.6-zts} +VERSIONS=${GATE_VERSIONS:-8.2 8.3 8.4 8.5 8.6 8.2-zts 8.3-zts 8.4-zts 8.5-zts 8.6-zts} case " $VERSIONS " in *" $REFERENCE "*) ;; *) VERSIONS="$REFERENCE $VERSIONS" ;; diff --git a/turbo-ext/bin/shared-core/fingerprint-linux.sh b/turbo-ext/bin/shared-core/fingerprint-linux.sh index 12d504f94ca..f2fdfea7c0e 100755 --- a/turbo-ext/bin/shared-core/fingerprint-linux.sh +++ b/turbo-ext/bin/shared-core/fingerprint-linux.sh @@ -7,12 +7,14 @@ # one version each; gate.py compare then needs only the .tsv files. # # Needs docker and an llvm-objdump (LLVM_OBJDUMP, else the newest found). +# GATE_BASE_IMAGE replaces ghcr.io/phpstan/turbo-build, e.g. with images +# built locally from .github/turbo-build. # Usage: SHARED_CORE_WORK_DIR= [GATE_VERSIONS="8.4 8.6-zts"] fingerprint-linux.sh set -euo pipefail S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} HERE=$(cd "$(dirname "$0")" && pwd) -VERSIONS=${GATE_VERSIONS:-8.3 8.4 8.5 8.6 8.3-zts 8.4-zts 8.5-zts 8.6-zts} +VERSIONS=${GATE_VERSIONS:-8.2 8.3 8.4 8.5 8.6 8.2-zts 8.3-zts 8.4-zts 8.5-zts 8.6-zts} if [ -z "${LLVM_OBJDUMP:-}" ]; then LLVM_OBJDUMP=$(command -v llvm-objdump || { ls /usr/bin/llvm-objdump-* /opt/homebrew/opt/llvm/bin/llvm-objdump 2>/dev/null || true; } | sort -V | tail -1) fi @@ -21,7 +23,7 @@ export LLVM_OBJDUMP # the CI build images plus clang for v in $VERSIONS; do - printf 'FROM ghcr.io/phpstan/turbo-build:gnu-php%s\nRUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq clang-14 > /dev/null && rm -rf /var/lib/apt/lists/*\n' "$v" \ + printf 'FROM %s:gnu-php%s\nRUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq clang-14 > /dev/null && rm -rf /var/lib/apt/lists/*\n' "${GATE_BASE_IMAGE:-ghcr.io/phpstan/turbo-build}" "$v" \ | docker build -q ${GATE_PLATFORM:+--platform "$GATE_PLATFORM"} -t "turbo-build-clang:gnu-php$v" - > /dev/null done diff --git a/turbo-ext/bin/shared-core/gate-linux.sh b/turbo-ext/bin/shared-core/gate-linux.sh index b508a8c611b..bac90458a26 100755 --- a/turbo-ext/bin/shared-core/gate-linux.sh +++ b/turbo-ext/bin/shared-core/gate-linux.sh @@ -5,7 +5,7 @@ # compares the results function by function. S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} WT=$(cd "$(dirname "$0")/../.." && pwd) -VERSIONS=${GATE_VERSIONS:-8.3 8.4 8.5 8.6} +VERSIONS=${GATE_VERSIONS:-8.2 8.3 8.4 8.5 8.6} JOBS=${GATE_JOBS:-3} # per version; the versions build at the same time # GATE_PLATFORM (e.g. linux/amd64) runs the images of another architecture — # code generation differs between them, and CI gates on x86_64 diff --git a/turbo-ext/bin/shared-core/link-split-linux.sh b/turbo-ext/bin/shared-core/link-split-linux.sh index 966744809c4..cb0d591e900 100755 --- a/turbo-ext/bin/shared-core/link-split-linux.sh +++ b/turbo-ext/bin/shared-core/link-split-linux.sh @@ -7,7 +7,7 @@ set -e S=${SHARED_CORE_WORK_DIR:?set SHARED_CORE_WORK_DIR to a scratch directory} REPO=$(cd "$(dirname "$0")/../../.." && pwd) -VERSIONS=${GATE_VERSIONS:-8.3 8.4 8.5 8.6} +VERSIONS=${GATE_VERSIONS:-8.2 8.3 8.4 8.5 8.6} CORE_FROM=${CORE_FROM:-8.4} OUT=$S/split-linux rm -rf "$OUT"; mkdir -p "$OUT" @@ -17,14 +17,14 @@ SHIM='src/Abi.o src/Shadow.o src/TrustedTypes.o src/main.o src/TerminateHandler. # statically folded libstdc++ out of both dynamic symbol tables FLAGS='-static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL -Wl,--gc-sections -Wl,-z,relro,-z,now -Wl,-z,noexecstack' -docker run --rm -v "$S/lx-$CORE_FROM:/build" -v "$OUT:/out" -w /build "ghcr.io/phpstan/turbo-build:gnu-php$CORE_FROM" sh -ec " +docker run --rm -v "$S/lx-$CORE_FROM:/build" -v "$OUT:/out" -w /build "${GATE_IMAGE:-ghcr.io/phpstan/turbo-build}:gnu-php$CORE_FROM" sh -ec " core=\$(find src -name '*.o' | grep -v -E 'src/(Abi|Shadow|TrustedTypes|main)\.o\$' | sort) g++ -shared $FLAGS -Wl,-soname,libphpstan_turbo_core.so -o /out/libphpstan_turbo_core.so \$core echo \"core exports: \$(nm -D --defined-only /out/libphpstan_turbo_core.so | wc -l)\" " for v in $VERSIONS; do - docker run --rm -v "$S/lx-$v:/build" -v "$OUT:/out" -v "$REPO:/repo:ro" -w /build "ghcr.io/phpstan/turbo-build:gnu-php$v" sh -ec " + docker run --rm -v "$S/lx-$v:/build" -v "$OUT:/out" -v "$REPO:/repo:ro" -w /build "${GATE_IMAGE:-ghcr.io/phpstan/turbo-build}:gnu-php$v" sh -ec " g++ -shared $FLAGS -Wl,-rpath,'\$ORIGIN' -o /out/phpstan_turbo-$v.so $SHIM -L/out -lphpstan_turbo_core # every extension symbol must come from the core: undefined pt_* # symbols would only fail at load time diff --git a/turbo-ext/composer.json b/turbo-ext/composer.json index e64a1984e6a..72e4ceb1ed3 100644 --- a/turbo-ext/composer.json +++ b/turbo-ext/composer.json @@ -7,7 +7,7 @@ "type": "php-ext", "homepage": "https://phpstan.org", "require": { - "php": "~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" }, "php-ext": { "extension-name": "phpstan_turbo", diff --git a/turbo-ext/src/Abi.cpp b/turbo-ext/src/Abi.cpp index 2e3ce0d961e..44928786d5a 100644 --- a/turbo-ext/src/Abi.cpp +++ b/turbo-ext/src/Abi.cpp @@ -12,6 +12,21 @@ #include "zend_fibers.h" #include "zend_weakrefs.h" +#if PHP_VERSION_ID < 80300 +#ifdef PHP_WIN32 +#include +#else +#include +#include +#include +#include +#endif +#ifdef __linux__ +#include +#include +#endif +#endif + static const zend_object_handlers *abi_object_handlers(const pt_abi_handlers &overrides) { @@ -30,6 +45,18 @@ static zend_function_entry *abi_function_entries(const reg::FunctionEntry *entri { zend_function_entry *functions = (zend_function_entry *) ecalloc(count + 1, sizeof(zend_function_entry)); for (size_t i = 0; i < count; i++) { +#if PHP_VERSION_ID < 80300 + /* before 8.3, an internal arginfo's class name is a C string under + * the plain name bit; the return type is entry 0 */ + if (entries[i].argInfo != NULL) { + for (uint32_t arg = 0; arg <= entries[i].numArgs; arg++) { + zend_type *type = &entries[i].argInfo[arg].type; + if (type->type_mask & _ZEND_TYPE_LITERAL_NAME_BIT) { + type->type_mask = (type->type_mask & ~_ZEND_TYPE_LITERAL_NAME_BIT) | _ZEND_TYPE_NAME_BIT; + } + } + } +#endif functions[i].fname = entries[i].fname; functions[i].handler = entries[i].handler; functions[i].arg_info = entries[i].argInfo; @@ -98,14 +125,143 @@ static void abi_weakrefs_hash_destroy(HashTable *ht) #endif } +/* {{{ the C stack limit before PHP 8.3 + * + * 8.3's engine knows the bounds of the C stack (EG(stack_base), + * EG(stack_limit)); before, this pair stands in for them, determined like + * zend_call_stack_init() determines them with the default settings, and + * switched along with the fresh stack segments of abi_run_on_fresh_stack(). + * Plain statics: PHPStan's processes run PHP on one thread. */ + +#if PHP_VERSION_ID < 80300 + +/* zend.reserved_stack_size's default in 8.3 (ZEND_ALLOCA_MAX_SIZE + 16 KiB) */ +#define PT_ABI_RESERVED_STACK_SIZE_LIMIT ((size_t) 48 * 1024) +/* zend_call_stack_default_size() of 8.3, for when the stack cannot be found */ +#define PT_ABI_DEFAULT_STACK_SIZE_LIMIT ((size_t) 2 * 1024 * 1024) + +static void *abi_stack_base = nullptr; +static void *abi_stack_limit = nullptr; + +/* the high address and the usable size of the current thread's stack */ +static bool abi_call_stack_get(void **base, size_t *maxSize) +{ +#if defined(__linux__) + if (getpid() == (pid_t) syscall(SYS_gettid)) { + /* the main thread: its mapping ends at the base, and it may grow by + * RLIMIT_STACK, up to the mapping below it */ + uintptr_t position = (uintptr_t) zend_call_stack_position(); + uintptr_t start, end, previousEnd = 0; + bool found = false; + char buffer[4096]; + FILE *maps = fopen("/proc/self/maps", "r"); + if (maps == NULL) return false; + while (fgets(buffer, sizeof(buffer), maps) && sscanf(buffer, "%" SCNxPTR "-%" SCNxPTR, &start, &end) == 2) { + if (start <= position && end >= position) { + found = true; + break; + } + previousEnd = end; + } + fclose(maps); + struct rlimit limit; + if (!found || getrlimit(RLIMIT_STACK, &limit) != 0 || limit.rlim_cur == RLIM_INFINITY) return false; + size_t size = limit.rlim_cur; + if (end - size < previousEnd) { + size = previousEnd - end; + } + *base = (void *) end; + *maxSize = size; + return true; + } + pthread_attr_t attr; + void *address; + size_t size; + if (pthread_getattr_np(pthread_self(), &attr) != 0) return false; + bool ok = pthread_attr_getstack(&attr, &address, &size) == 0; + if (ok) { + size_t guard = 0; + /* glibc counts the guard area into the size, musl does not */ +#ifdef __GLIBC__ + if (pthread_attr_getguardsize(&attr, &guard) != 0) guard = 0; +#endif + *base = (int8_t *) address + size; + *maxSize = size - guard; + } + pthread_attr_destroy(&attr); + return ok; +#elif defined(__APPLE__) + *base = pthread_get_stackaddr_np(pthread_self()); + *maxSize = pthread_get_stacksize_np(pthread_self()); + return true; +#elif defined(PHP_WIN32) + /* the reserved region less its guard pages and the last uncommitted + * page, as 8.3 computes it. 8.3 reads the limits with + * GetCurrentThreadStackLimits, which 8.2's _WIN32_WINNT does not declare; + * the stack's allocation base and the TEB's StackBase are the same limits */ + MEMORY_BASIC_INFORMATION stack = {0}, guard = {0}, uncommitted = {0}; + if (VirtualQuery(&stack, &stack, sizeof(stack)) < sizeof(stack)) return false; + ULONG_PTR lowLimit = (ULONG_PTR) stack.AllocationBase; + ULONG_PTR highLimit = (ULONG_PTR) ((NT_TIB *) NtCurrentTeb())->StackBase; + if (VirtualQuery((void *) lowLimit, &uncommitted, sizeof(uncommitted)) < sizeof(uncommitted)) return false; + if (VirtualQuery((int8_t *) uncommitted.BaseAddress + uncommitted.RegionSize, &guard, sizeof(guard)) < sizeof(guard)) return false; + size_t size = (size_t) (highLimit - lowLimit); + size_t pageSize = zend_get_page_size(); + if (size <= guard.RegionSize + pageSize) return false; + *base = (void *) highLimit; + *maxSize = size - guard.RegionSize - pageSize; + return true; +#else + (void) base; + (void) maxSize; + return false; +#endif +} + +static void *abi_call_stack_limit(void *base, size_t size, size_t reserved) +{ + if (UNEXPECTED(size > (uintptr_t) base)) { + return nullptr; + } + return (int8_t *) base - size + reserved; +} + +static void abi_call_stack_init() +{ + void *base; + size_t size; + if (!abi_call_stack_get(&base, &size)) { + /* as 8.3 falls back: from here, which is not the actual base */ + base = zend_call_stack_position(); + size = PT_ABI_DEFAULT_STACK_SIZE_LIMIT - 32 * 1024; + } + abi_stack_base = base; + abi_stack_limit = abi_call_stack_limit(base, size, PT_ABI_RESERVED_STACK_SIZE_LIMIT); +} + +/* zend_fiber_stack is private to zend_fibers.c; 8.2's begins with these */ +struct AbiFiberStack +{ + void *pointer; + size_t size; +}; + +#endif + +/* }}} */ + static void abi_call_stack_size_error() { #if PHP_VERSION_ID >= 80400 zend_call_stack_size_error(); -#else +#elif PHP_VERSION_ID >= 80300 /* static in PHP 8.3; the same message */ zend_throw_error(nullptr, "Maximum call stack size of %zu bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?", (size_t) ((uintptr_t) EG(stack_base) - (uintptr_t) EG(stack_limit))); +#else + /* 8.3's message, without the settings 8.2 does not have */ + zend_throw_error(nullptr, "Maximum call stack size of %zu bytes reached. Infinite recursion?", + (size_t) ((uintptr_t) abi_stack_base - (uintptr_t) abi_stack_limit)); #endif } @@ -164,8 +320,13 @@ static const zend_expected_type abi_expected_types[] = { Z_EXPECTED_DOUBLE_OR_NULL, Z_EXPECTED_NUMBER, Z_EXPECTED_NUMBER_OR_NULL, +#if PHP_VERSION_ID >= 80300 Z_EXPECTED_NUMBER_OR_STRING, Z_EXPECTED_NUMBER_OR_STRING_OR_NULL, +#else + Z_EXPECTED_LONG, /* not produced before 8.3 */ + Z_EXPECTED_LONG, /* not produced before 8.3 */ +#endif Z_EXPECTED_ARRAY_OR_STRING, Z_EXPECTED_ARRAY_OR_STRING_OR_NULL, Z_EXPECTED_STRING_OR_LONG, @@ -205,7 +366,8 @@ static bool abi_parse_arg_bool_slow(const zval *arg, bool *dest, uint32_t num) *dest = status; return true; #else - return zend_parse_arg_bool_slow(arg, dest, num); + /* the zval was not const up to 8.2; neither function writes it */ + return zend_parse_arg_bool_slow((zval *) arg, dest, num); #endif } @@ -215,7 +377,7 @@ static bool abi_parse_arg_double_slow(const zval *arg, double *dest, uint32_t nu *dest = zend_parse_arg_double_slow(arg, num); return !zend_isnan(*dest); #else - return zend_parse_arg_double_slow(arg, dest, num); + return zend_parse_arg_double_slow((zval *) arg, dest, num); #endif } @@ -274,9 +436,20 @@ ZEND_STACK_ALIGNED void freshStackCoroutine(zend_fiber_transfer *transfer) (void) transfer; /* already addresses the caller, which is where it returns */ FreshStackCall *call = pt_fresh_stack_call; zend_fiber_context *context = EG(current_fiber_context); -#ifdef ZEND_CHECK_STACK_LIMIT +#if PHP_VERSION_ID >= 80300 EG(stack_base) = zend_fiber_stack_base(context->stack); EG(stack_limit) = zend_fiber_stack_limit(context->stack); +#else + /* zend_fiber_stack_base() / _limit() of 8.3 */ + const AbiFiberStack *stack = (const AbiFiberStack *) context->stack; + abi_stack_base = (int8_t *) stack->pointer + stack->size; +#ifdef __APPLE__ + /* Apple clang's ___chkstk_darwin probes twice the frame size away on + * an alternative stack (8.3 reserves twice as much there) */ + abi_stack_limit = (int8_t *) stack->pointer + 2 * PT_ABI_RESERVED_STACK_SIZE_LIMIT; +#else + abi_stack_limit = (int8_t *) stack->pointer + PT_ABI_RESERVED_STACK_SIZE_LIMIT; +#endif #endif zend_try { call->body(call->data); @@ -296,7 +469,13 @@ static void abi_run_on_fresh_stack(void (*body)(void *), void *data) { zend_fiber_context context; memset(&context, 0, sizeof(context)); - if (UNEXPECTED(zend_fiber_init_context(&context, &pt_fresh_stack_kind, freshStackCoroutine, PT_ENGINE_FRESH_STACK_SIZE_LIMIT) == FAILURE)) { +#if PHP_VERSION_ID >= 80300 + bool initialized = zend_fiber_init_context(&context, &pt_fresh_stack_kind, freshStackCoroutine, PT_ENGINE_FRESH_STACK_SIZE_LIMIT) == SUCCESS; +#else + /* returned bool up to 8.2 */ + bool initialized = zend_fiber_init_context(&context, &pt_fresh_stack_kind, freshStackCoroutine, PT_ENGINE_FRESH_STACK_SIZE_LIMIT); +#endif + if (UNEXPECTED(!initialized)) { /* no stack to be had: continue here, where the engine's own stack * limit decides */ if (EG(exception) != NULL) { @@ -313,9 +492,18 @@ static void abi_run_on_fresh_stack(void (*body)(void *), void *data) transfer.context = &context; transfer.flags = 0; ZVAL_NULL(&transfer.value); +#if PHP_VERSION_ID < 80300 + /* 8.3's switch restores the engine's pair, this one is restored here */ + void *stackBase = abi_stack_base; + void *stackLimit = abi_stack_limit; +#endif /* returns once the body finished; the dead context is destroyed then */ zend_fiber_switch_context(&transfer); pt_fresh_stack_call = previous; +#if PHP_VERSION_ID < 80300 + abi_stack_base = stackBase; + abi_stack_limit = stackLimit; +#endif EG(vm_stack) = call.vmStack; EG(vm_stack_top) = call.vmStackTop; @@ -346,6 +534,14 @@ void pt_abi_init() #undef PT_ABI_CG_FIELD #undef PT_ABI_KNOWN_STRING #undef PT_ABI_OBJECT_HANDLER +#if PHP_VERSION_ID >= 80300 + pt_abi.eg_stack_base = &EG(stack_base); + pt_abi.eg_stack_limit = &EG(stack_limit); +#else + abi_call_stack_init(); + pt_abi.eg_stack_base = &abi_stack_base; + pt_abi.eg_stack_limit = &abi_stack_limit; +#endif pt_abi.offset_internal_function_handler = offsetof(zend_internal_function, handler); pt_abi.sizeof_arg_info = sizeof(zend_arg_info); @@ -379,4 +575,9 @@ void pt_abi_init() pt_abi.stream_read = abi_stream_read; pt_abi.run_on_fresh_stack = abi_run_on_fresh_stack; pt_abi.is_true = [](const zval *op) -> bool { return zend_is_true((zval *) op); }; +#if PHP_VERSION_ID >= 80300 + pt_abi.zval_value_name = zend_zval_value_name; +#else + pt_abi.zval_value_name = zend_zval_type_name; +#endif } diff --git a/turbo-ext/src/Engine.h b/turbo-ext/src/Engine.h index 5b9bd9afed0..3a6289abf2c 100644 --- a/turbo-ext/src/Engine.h +++ b/turbo-ext/src/Engine.h @@ -28,9 +28,6 @@ #include #include "zend_exceptions.h" -#ifdef ZEND_CHECK_STACK_LIMIT -#include "zend_call_stack.h" -#endif /* {{{ native closures * @@ -342,7 +339,7 @@ inline void pt_finally(F &&cleanup) static zend_always_inline bool pt_engine_stack_low() { #ifdef ZEND_CHECK_STACK_LIMIT - uintptr_t limit = (uintptr_t) EG(stack_limit); + uintptr_t limit = (uintptr_t) *pt_abi.eg_stack_limit; if (limit == 0) return false; uintptr_t position = (uintptr_t) zend_call_stack_position(); return position <= limit || position - limit < PT_ENGINE_STACK_MARGIN_LIMIT; diff --git a/turbo-ext/src/GenericObjectType.cpp b/turbo-ext/src/GenericObjectType.cpp index ebf0fe393bb..4cc4b2830df 100644 --- a/turbo-ext/src/GenericObjectType.cpp +++ b/turbo-ext/src/GenericObjectType.cpp @@ -22,10 +22,6 @@ namespace sigs = ptdecl::GenericObjectType::sig; #include -#ifdef ZEND_CHECK_STACK_LIMIT -# include "zend_call_stack.h" -#endif - zend_class_entry *pt_ce_generic_object_type = nullptr; /* The twin describes its type arguments through an array_map() closure, and diff --git a/turbo-ext/src/abi.h b/turbo-ext/src/abi.h index 6f97e5f65a6..f1cf18821ee 100644 --- a/turbo-ext/src/abi.h +++ b/turbo-ext/src/abi.h @@ -18,14 +18,56 @@ #include +/* zend_fcall_info_cache in 8.3's layout over older headers (support.h) */ +#ifdef PT_ABI_FCALL_INFO_CACHE_BEFORE_83 +#undef _zend_fcall_info_cache +#undef zend_fcall_info_cache +struct _zend_fcall_info_cache : pt_abi_zend_fcall_info_cache_before_83_t +{ + zend_object *closure; +}; +typedef struct _zend_fcall_info_cache zend_fcall_info_cache; +#endif + +/* The C stack limit arrived in PHP 8.3 (zend_call_stack.h, EG(stack_base), + * EG(stack_limit)). Against older headers, the two inline helpers the + * shared code uses are spelled out as 8.3 has them, and the limit is + * Abi.cpp's own (pt_abi_globals::eg_stack_limit). */ +#if PHP_VERSION_ID >= 80300 +#ifdef ZEND_CHECK_STACK_LIMIT +#include "zend_call_stack.h" +#endif +#else +#define ZEND_CHECK_STACK_LIMIT 1 +#ifdef _MSC_VER +#include +#endif +static zend_always_inline void *zend_call_stack_position(void) +{ +#ifdef _MSC_VER + return _AddressOfReturnAddress(); +#else + return __builtin_frame_address(0); +#endif +} +static zend_always_inline bool zend_call_stack_overflowed(void *stack_limit) +{ + return (uintptr_t) zend_call_stack_position() <= (uintptr_t) stack_limit; +} +#endif + +/* 8.3 marks the class names of internal arginfo with a bit of their own; + * Abi.cpp turns it into the name bit the older engines expect */ +#ifndef _ZEND_TYPE_LITERAL_NAME_BIT +#define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23) +#endif + #define PT_ABI_EG_FIELDS(X) \ X(exception) \ X(uninitialized_zval) \ X(class_table) \ X(function_table) \ X(fake_scope) \ - X(stack_limit) \ - X(stack_base) \ X(current_module) \ X(weakrefs) \ X(vm_stack) \ @@ -129,6 +171,10 @@ struct pt_abi_globals #undef PT_ABI_CG_FIELD #undef PT_ABI_KNOWN_STRING #undef PT_ABI_OBJECT_HANDLER + /* EG(stack_base) / EG(stack_limit) from 8.3 on; before, Abi.cpp's own + * pair, determined like 8.3's zend_call_stack_init() */ + void **eg_stack_base; + void **eg_stack_limit; /* member offsets that moved between minors */ size_t offset_internal_function_handler; /* 8.4 inserted frameless_function_infos and doc_comment before it */ @@ -176,6 +222,9 @@ struct pt_abi_globals void (*run_on_fresh_stack)(void (*body)(void *), void *data); /* zend_is_true() where the declaration cannot be renamed (MSVC, below) */ bool (*is_true)(const zval *op); + /* what the engine's own type errors name a value by: its type up to 8.2 + * ("bool given"), from 8.3 its value for true/false ("true given") */ + const char *(*zval_value_name)(const zval *arg); }; /* Defined by the shared core, filled by the version-specific library, which @@ -280,6 +329,104 @@ static zend_always_inline void pt_abi_array_init_size(zval *arg, uint32_t size) } \ } while (0) +/* ZEND_ASSUME() (and ZEND_ASSERT() over it): from 8.3 on, clang's + * __builtin_assume() — a hint clang compiles differently from the + * __builtin_unreachable() form 8.2 spells for every compiler */ +#if defined(__clang__) && defined(__has_builtin) +#if __has_builtin(__builtin_assume) +#pragma clang diagnostic ignored "-Wassume" +#undef ZEND_ASSUME +#define ZEND_ASSUME(c) __builtin_assume(c) +#endif +#endif + +/* zend_string.h's allocating helpers: 8.2's late patch releases spell them + * as wrappers over fallible variants (zend_string_falloc(), ...), the same + * result with fallible = false. 8.5's definitions — also where the headers' + * macros expand to them (ZVAL_STRINGL(), RETURN_STRING(), ...). */ +static zend_always_inline zend_string *pt_abi_zend_string_alloc(size_t len, bool persistent) +{ + zend_string *ret = (zend_string *) pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); + GC_SET_REFCOUNT(ret, 1); + GC_TYPE_INFO(ret) = GC_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT); + ZSTR_H(ret) = 0; + ZSTR_LEN(ret) = len; + return ret; +} +static zend_always_inline zend_string *pt_abi_zend_string_init(const char *str, size_t len, bool persistent) +{ + zend_string *ret = pt_abi_zend_string_alloc(len, persistent); + memcpy(ZSTR_VAL(ret), str, len); + ZSTR_VAL(ret)[len] = '\0'; + return ret; +} +static zend_always_inline zend_string *pt_abi_zend_string_dup(zend_string *s, bool persistent) +{ + if (GC_FLAGS(s) & IS_STR_INTERNED) { + return s; + } + return pt_abi_zend_string_init(ZSTR_VAL(s), ZSTR_LEN(s), persistent); +} +static zend_always_inline zend_string *pt_abi_zend_string_realloc(zend_string *s, size_t len, bool persistent) +{ + zend_string *ret; + if (!(GC_FLAGS(s) & IS_STR_INTERNED)) { + if (EXPECTED(GC_REFCOUNT(s) == 1)) { + ret = (zend_string *) perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); + ZSTR_LEN(ret) = len; + zend_string_forget_hash_val(ret); + return ret; + } + } + ret = pt_abi_zend_string_alloc(len, persistent); + memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), MIN(len, ZSTR_LEN(s)) + 1); + if (!(GC_FLAGS(s) & IS_STR_INTERNED)) { + GC_DELREF(s); + } + return ret; +} +static zend_always_inline zend_string *pt_abi_smart_str_extract(smart_str *str) +{ + if (str->s == NULL) { + return ZSTR_EMPTY_ALLOC(); + } + ZSTR_VAL(str->s)[ZSTR_LEN(str->s)] = '\0'; + if (str->a > ZSTR_LEN(str->s)) { + str->s = pt_abi_zend_string_realloc(str->s, ZSTR_LEN(str->s), false); + str->a = ZSTR_LEN(str->s); + } + zend_string *res = str->s; + str->s = NULL; + return res; +} +/* variadic, like the smart_str ones below: PT_LC() expands to two arguments */ +#define zend_string_alloc(...) pt_abi_zend_string_alloc(__VA_ARGS__) +#define zend_string_init(...) pt_abi_zend_string_init(__VA_ARGS__) +#define zend_string_dup(...) pt_abi_zend_string_dup(__VA_ARGS__) +#define zend_string_realloc(...) pt_abi_zend_string_realloc(__VA_ARGS__) +#define smart_str_extract(...) pt_abi_smart_str_extract(__VA_ARGS__) + +/* 8.3's packed fill keeps nNumOfElements right when the array is not empty + * to begin with; 8.2's sets it to the fill index (the same from an empty + * array). 8.5's definitions. */ +#undef ZEND_HASH_FILL_GROW +#define ZEND_HASH_FILL_GROW() do { \ + if (UNEXPECTED(__fill_idx >= __fill_ht->nTableSize)) { \ + __fill_ht->nNumOfElements += __fill_idx - __fill_ht->nNumUsed; \ + __fill_ht->nNumUsed = __fill_idx; \ + __fill_ht->nNextFreeElement = __fill_idx; \ + zend_hash_packed_grow(__fill_ht); \ + __fill_val = __fill_ht->arPacked + __fill_idx; \ + } \ + } while (0); +#undef ZEND_HASH_FILL_FINISH +#define ZEND_HASH_FILL_FINISH() do { \ + __fill_ht->nNumOfElements += __fill_idx - __fill_ht->nNumUsed; \ + __fill_ht->nNumUsed = __fill_idx; \ + __fill_ht->nNextFreeElement = __fill_idx; \ + __fill_ht->nInternalPointer = 0; \ + } while (0) + /* zend_smart_str.h's appending helpers: recent patch releases (8.3.36 and * its siblings) grow the buffer through smart_str_ferealloc(), which the * earlier patch releases of every minor do not export — a core built @@ -643,6 +790,10 @@ static zend_always_inline void *pt_abi_object_alloc(size_t obj_size, zend_class_ #define zend_dval_to_lval(d) (pt_abi.dval_to_lval(d)) +/* exported from 8.3 on; the type errors the shared code raises name the + * value the way the running engine's own do */ +#define zend_zval_value_name(arg) (pt_abi.zval_value_name(arg)) + /* 8.5 destroys the previous value with zval_ptr_safe_dtor() */ #undef ZEND_TRY_ASSIGN_REF_STR #define ZEND_TRY_ASSIGN_REF_STR(zv, str) pt_abi_try_assign_ref_str(zv, str) diff --git a/turbo-ext/src/reg.h b/turbo-ext/src/reg.h index 5dce507f7bc..ecc85a50255 100644 --- a/turbo-ext/src/reg.h +++ b/turbo-ext/src/reg.h @@ -561,10 +561,10 @@ inline void declareMembers(zend_class_entry *ce, const std::vector &pr size_t partLen = end != NULL ? (size_t) (end - start) : strlen(start); zend_string *className = zend_new_interned_string(zend_string_init(start, partLen, persistent)); zend_alloc_ce_cache(className); - list->types[i] = (zend_type) ZEND_TYPE_INIT_CLASS(className, 0, 0); + list->types[i] = ZEND_TYPE_INIT_CLASS(className, 0, 0); start = end != NULL ? end + 1 : start; } - type = (zend_type) ZEND_TYPE_INIT_UNION(list, (property.defaultValue & MAY_BE_NULL) != 0 ? MAY_BE_NULL : 0); + type = ZEND_TYPE_INIT_UNION(list, (property.defaultValue & MAY_BE_NULL) != 0 ? MAY_BE_NULL : 0); } else if (property.className != NULL) { /* a class-typed property, declared the way the compiler * declares one (zend_compile_single_typename): an interned @@ -578,9 +578,9 @@ inline void declareMembers(zend_class_entry *ce, const std::vector &pr zend_alloc_ce_cache(className); /* the bits beyond MAY_BE_NULL in defaultValue are the scalar * members of a `Foo|false|null` union */ - type = (zend_type) ZEND_TYPE_INIT_CLASS(className, (property.defaultValue & MAY_BE_NULL) != 0, (uint32_t) property.defaultValue & ~(uint32_t) MAY_BE_NULL); + type = ZEND_TYPE_INIT_CLASS(className, (property.defaultValue & MAY_BE_NULL) != 0, (uint32_t) property.defaultValue & ~(uint32_t) MAY_BE_NULL); } else { - type = (zend_type) ZEND_TYPE_INIT_MASK((uint32_t) property.defaultValue); + type = ZEND_TYPE_INIT_MASK((uint32_t) property.defaultValue); } zend_declare_typed_property(ce, nameStr, &defaultValue, property.visibility, NULL, type); zend_string_release(nameStr); @@ -591,7 +591,7 @@ inline void declareMembers(zend_class_entry *ce, const std::vector &pr zend_string *nameStr = zend_string_init(property.name, len, persistent); zval defaultValue; ZVAL_BOOL(&defaultValue, property.defaultValue != 0); - zend_type type = (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL); + zend_type type = ZEND_TYPE_INIT_MASK(MAY_BE_BOOL); zend_declare_typed_property(ce, nameStr, &defaultValue, property.visibility, NULL, type); zend_string_release(nameStr); break; @@ -604,7 +604,7 @@ inline void declareMembers(zend_class_entry *ce, const std::vector &pr zend_string *nameStr = zend_string_init(property.name, len, persistent); zval defaultValue; ZVAL_STR(&defaultValue, zend_string_init_interned(property.stringValue, strlen(property.stringValue), persistent)); - zend_type type = (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING); + zend_type type = ZEND_TYPE_INIT_MASK(MAY_BE_STRING); zend_declare_typed_property(ce, nameStr, &defaultValue, property.visibility, NULL, type); zend_string_release(nameStr); break; @@ -614,7 +614,7 @@ inline void declareMembers(zend_class_entry *ce, const std::vector &pr zend_string *nameStr = zend_string_init(property.name, len, persistent); zval defaultValue; ZVAL_LONG(&defaultValue, property.defaultValue); - zend_type type = (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG); + zend_type type = ZEND_TYPE_INIT_MASK(MAY_BE_LONG); zend_declare_typed_property(ce, nameStr, &defaultValue, property.visibility, NULL, type); zend_string_release(nameStr); break; diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index db10e77bc89..4e548b663df 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -53,11 +53,26 @@ extern "C" { #define CG(v) ZEND_TSRMG_FAST(compiler_globals_offset, zend_compiler_globals *, v) #endif +/* PHP 8.3 appended `closure` to zend_fcall_info_cache. Against older headers + * the shared code declares its own with 8.3's layout (abi.h), on top of the + * headers' four-member struct, declared under another name here — so the + * shared code compiles to the same frames against every header set. The + * older engines read and write the first four members only, and nothing in + * the shared code reads `closure`. The engine's structures embedding one by + * value keep their layout only when declared before abi.h: their headers + * are included below. */ +#if PHP_VERSION_ID < 80300 && !defined(PHPSTANTURBO_ABI_IMPL) +#define _zend_fcall_info_cache pt_abi_zend_fcall_info_cache_before_83 +#define zend_fcall_info_cache pt_abi_zend_fcall_info_cache_before_83_t +#define PT_ABI_FCALL_INFO_CACHE_BEFORE_83 +#endif + extern "C" { #include "php.h" #include "zend_exceptions.h" #include "zend_interfaces.h" #include "zend_smart_str.h" +#include "zend_fibers.h" } #pragma GCC diagnostic pop diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index c6a7f582b90..30802a6f19c 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -414,8 +414,13 @@ // ---- StringType / ConstantStringType / ClassStringType / GenericClassStringType ---- // a reflection provider behind ReflectionProviderStaticAccessor, as the // class-string and callable queries need one (an existing class name, a -// known function, a static method); the PhpVersion accessor for isCallable() -$stringContainer = (new \PHPStan\DependencyInjection\ContainerFactory($root))->create(sys_get_temp_dir() . '/phpstan-turbo-type-family', [], []); +// known function, a static method); the PhpVersion accessor for isCallable(). +// Before PHP 8.3, the object sections reflect 8.3's Random\IntervalBoundary +// as their enum from the stubs, and the initializer section's fixture (typed +// class constants) is read as an analysed file instead of being loaded +$initializerFixture = __DIR__ . '/type-family-initializer-fixture.php'; +$fixtureAnalysedPaths = PHP_VERSION_ID < 80300 ? [$initializerFixture] : []; +$stringContainer = (new \PHPStan\DependencyInjection\ContainerFactory($root))->create(sys_get_temp_dir() . '/phpstan-turbo-type-family', [], $fixtureAnalysedPaths, [], [], \PHPStan\Command\CommandHelper::DEFAULT_LEVEL, null, null, null, null, PHP_VERSION_ID < 80300 ? ['phpVersion' => 80300] : []); $stringReflectionProvider = $stringContainer->getByType(\PHPStan\Reflection\ReflectionProvider::class); \PHPStan\Reflection\ReflectionProviderStaticAccessor::registerInstance($stringReflectionProvider); \PHPStan\Reflection\PhpVersionStaticAccessor::registerInstance($stringContainer->getByType(\PHPStan\Php\PhpVersion::class)); @@ -7661,7 +7666,9 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu // through a bound closure, and getType() over a parsed corpus of constant // expressions in an empty, a class and a trait context (class constants of // the fixture: typed / untyped / final / enum / cyclic) -require_once __DIR__ . '/type-family-initializer-fixture.php'; +if (PHP_VERSION_ID >= 80300) { + require_once $initializerFixture; +} $observations['native PHPStan\Reflection\InitializerExprTypeResolver'] = (new ReflectionMethod(\PHPStan\Reflection\InitializerExprTypeResolver::class, 'getType'))->isInternal(); { $resolver = $stringContainer->getByType(\PHPStan\Reflection\InitializerExprTypeResolver::class); @@ -7861,7 +7868,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu 'oversizedUnpack' => array_merge(array_map(static fn (int $i) => $item($int($i)), range(0, 260)), [$item($operands['constShape'], null, true)]), ]; foreach ([70400, 80400] as $phpVersionId) { - $versionContainer = (new \PHPStan\DependencyInjection\ContainerFactory($root))->create(sys_get_temp_dir() . '/phpstan-turbo-type-family-' . $phpVersionId, [], [], [], [], \PHPStan\Command\CommandHelper::DEFAULT_LEVEL, null, null, null, null, ['phpVersion' => $phpVersionId]); + $versionContainer = (new \PHPStan\DependencyInjection\ContainerFactory($root))->create(sys_get_temp_dir() . '/phpstan-turbo-type-family-' . $phpVersionId, [], $fixtureAnalysedPaths, [], [], \PHPStan\Command\CommandHelper::DEFAULT_LEVEL, null, null, null, null, ['phpVersion' => $phpVersionId]); $versionResolver = $versionContainer->getByType(\PHPStan\Reflection\InitializerExprTypeResolver::class); foreach ($arrays as $name => $items) { $r["getArrayType $phpVersionId $name"] = $catching(static fn () => $versionResolver->getArrayType(new \PhpParser\Node\Expr\Array_($items), $getType)); @@ -7938,7 +7945,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu } } } - $pathContainer = (new \PHPStan\DependencyInjection\ContainerFactory($root))->create(sys_get_temp_dir() . '/phpstan-turbo-type-family-paths', [], [], [], [], \PHPStan\Command\CommandHelper::DEFAULT_LEVEL, null, null, null, null, ['usePathConstantsAsConstantString' => true]); + $pathContainer = (new \PHPStan\DependencyInjection\ContainerFactory($root))->create(sys_get_temp_dir() . '/phpstan-turbo-type-family-paths', [], $fixtureAnalysedPaths, [], [], \PHPStan\Command\CommandHelper::DEFAULT_LEVEL, null, null, null, null, ['usePathConstantsAsConstantString' => true]); $pathResolver = $pathContainer->getByType(\PHPStan\Reflection\InitializerExprTypeResolver::class); foreach ($exprs as $i => $expr) { if (!$expr instanceof \PhpParser\Node\Scalar\MagicConst\File && !$expr instanceof \PhpParser\Node\Scalar\MagicConst\Dir) { diff --git a/turbo-ext/tests/walk-trace.php b/turbo-ext/tests/walk-trace.php index e6d58251324..71832018651 100644 --- a/turbo-ext/tests/walk-trace.php +++ b/turbo-ext/tests/walk-trace.php @@ -52,9 +52,15 @@ function walkTraceFiles(string $root, array $paths): array } $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)); foreach ($iterator as $file) { - if ($file->isFile() && str_ends_with($file->getFilename(), '.php')) { - $files[] = $file->getRealPath(); + if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) { + continue; } + // the twin throws the engine's C stack limit there, which arrived + // in PHP 8.3: before, it overflows the C stack + if (PHP_VERSION_ID < 80300 && $file->getFilename() === 'describe-recursion-stack-limit.php') { + continue; + } + $files[] = $file->getRealPath(); } } sort($files);