From 31b41e9cfb98846291ec9cbe2edf9cff711cb2db Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 12:58:11 +0000 Subject: [PATCH 1/4] ci: test ZDebug on the PHP 8.6 beta via z-engine 8.6.x-dev z-engine master supports PHP 8.6 (branch-aliased 8.6.x-dev) with linux and darwin definitions committed, so ZDebug can ride it the same way it rides 8.4.x-dev and 8.5.x-dev: - composer: accept lisachenko/z-engine 8.6.x-dev; Composer picks the matching line per running PHP minor as before - ci: add PHP 8.6 legs on ubuntu-latest, macos-latest (arm64) and macos-15-intel (x64) to both the test and PHPStan matrices - docs: extend the supported-version wording to 8.4 / 8.5 / 8.6 (beta) Verified locally on PHP 8.6.0beta2 (NTS): full suite (291 tests) and the integration gate (41 tests, fail-on-skipped) green, PHPStan level max clean, cs check clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG --- .github/workflows/ci.yml | 11 ++++++++--- AGENTS.md | 9 +++++---- README.md | 6 +++--- composer.json | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff609a8..e097871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,20 @@ jobs: fail-fast: false matrix: include: + # PHP 8.6 is still in beta: setup-php installs the beta build and + # Composer resolves z-engine 8.6.x-dev (the master branch alias). - { os: ubuntu-latest, php: '8.4' } - { os: ubuntu-latest, php: '8.5' } - # z-engine ships darwin (x64 + arm64) definitions for both the 8.4 - # and 8.5 lines (lisachenko/z-engine#58), so macOS runs the full PHP + - { os: ubuntu-latest, php: '8.6' } + # z-engine ships darwin (x64 + arm64) definitions for the 8.4, 8.5 + # and 8.6 lines (lisachenko/z-engine#58), so macOS runs the full PHP # matrix. macos-latest is arm64, macos-15-intel is x64. - { os: macos-latest, php: '8.4' } - { os: macos-latest, php: '8.5' } + - { os: macos-latest, php: '8.6' } - { os: macos-15-intel, php: '8.4' } - { os: macos-15-intel, php: '8.5' } + - { os: macos-15-intel, php: '8.6' } steps: - uses: actions/checkout@v4 @@ -50,7 +55,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.4', '8.5'] + php: ['8.4', '8.5', '8.6'] steps: - uses: actions/checkout@v4 diff --git a/AGENTS.md b/AGENTS.md index 46196ec..da06867 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,13 +11,14 @@ split: prose says ZDebug, code says `zdebug`. ## Branch model -`main` supports PHP 8.4 and 8.5 in parallel. Engine memory layouts change between PHP -minors, so each minor rides its own z-engine line (`8.4.x-dev` on PHP 8.4, `8.5.x-dev` -on PHP 8.5) and Composer resolves the matching one for the running PHP. +`main` supports PHP 8.4, 8.5 and 8.6 (beta) in parallel. Engine memory layouts change +between PHP minors, so each minor rides its own z-engine line (`8.4.x-dev` on PHP 8.4, +`8.5.x-dev` on PHP 8.5, `8.6.x-dev` — the master branch alias — on PHP 8.6) and +Composer resolves the matching one for the running PHP. ## Environment requirements -- PHP `^8.4` (8.4 and 8.5 are tested in CI), **NTS, linux-x64 or darwin-x64/arm64** (the platforms z-engine ships definitions for). +- PHP `^8.4` (8.4, 8.5 and the 8.6 beta are tested in CI), **NTS, linux-x64 or darwin-x64/arm64** (the platforms z-engine ships definitions for). - `ffi.enable=1` and **JIT off** (`opcache.jit=off`) — the JIT rewrites the executor internals the statement hook plugs into. Both must come from `php.ini`/`-d`. - The debuggee's code must be compiled **after** the debugger initializes; the diff --git a/README.md b/README.md index 112576a..a408cc6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **ZDebug** is an Xdebug-compatible step debugger with **no C extension**. Your IDE attaches over DBGp — Xdebug's own protocol — while pure PHP code drives the Zend VM through FFI, courtesy of [z-engine](https://github.com/lisachenko/z-engine). Set breakpoints, step through code, inspect and edit the stack and variables in PhpStorm or VS Code, with nothing compiled and nothing installed but Composer packages. [![CI](https://img.shields.io/github/actions/workflow/status/lisachenko/zdebug/ci.yml?branch=main&label=CI)](https://github.com/lisachenko/zdebug/actions/workflows/ci.yml) -[![PHP Version](https://img.shields.io/badge/php-8.4%20%7C%208.5-8892BF.svg)](https://php.net/) +[![PHP Version](https://img.shields.io/badge/php-8.4%20%7C%208.5%20%7C%208.6-8892BF.svg)](https://php.net/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![PHPStan](https://img.shields.io/badge/PHPStan-level%20max-brightgreen.svg)](https://phpstan.org/) [![Status](https://img.shields.io/badge/status-experimental-orange.svg)](#status) @@ -161,13 +161,13 @@ Precedence, lowest to highest: built-in defaults → Xdebug ini/env → `ZDEBUG_ ## Requirements -- PHP **8.4 or 8.5** (supported in parallel), **NTS**, **linux-x64 or macOS x64/arm64** (platforms z-engine ships definitions for); Composer resolves the matching z-engine line per minor +- PHP **8.4, 8.5 or 8.6 (beta)** (supported in parallel), **NTS**, **linux-x64 or macOS x64/arm64** (platforms z-engine ships definitions for); Composer resolves the matching z-engine line per minor - `ffi.enable=1` and **`opcache.jit=off`** (the JIT rewrites the executor internals the hook plugs into) - Your app's code must load **after** ZDebug attaches — `auto_prepend_file` guarantees this ## Status -The debugger is complete enough to use as one: an IDE attaches, sets line, conditional, call, return and exception breakpoints, steps over/into/out, walks the stack, reads and writes variables, evaluates expressions, fetches source and detaches — all of it proven end-to-end by a test suite that plays a fake IDE against a real spawned child process, on PHP 8.4 and 8.5 across linux-x64 and macOS (x64 + arm64). +The debugger is complete enough to use as one: an IDE attaches, sets line, conditional, call, return and exception breakpoints, steps over/into/out, walks the stack, reads and writes variables, evaluates expressions, fetches source and detaches — all of it proven end-to-end by a test suite that plays a fake IDE against a real spawned child process, on PHP 8.4, 8.5 and 8.6 (beta) across linux-x64 and macOS (x64 + arm64). What is left is a short list, and most of it is the engine's doing rather than unfinished plumbing: diff --git a/composer.json b/composer.json index 8da35a4..5f97c30 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^8.4", "ext-ffi": "*", - "lisachenko/z-engine": "8.4.x-dev || 8.5.x-dev" + "lisachenko/z-engine": "8.4.x-dev || 8.5.x-dev || 8.6.x-dev" }, "require-dev": { "phpunit/phpunit": "^12.2", From db23e9c68fd4ea63fa126974e5d809ea70097413 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 13:08:16 +0000 Subject: [PATCH 2/4] ci: add temporary arm64/PHP 8.6 diagnostic workflow The Tests (PHP 8.6, macos-latest) leg fails with child crashes (exit 11) and corrupted function dispatch while the identical PHP build passes on macos-15-intel, and z-engine's own arm64 8.6 suite is green. This manually-dispatched workflow layers the boot sequence (Core::init -> EXT_STMT compile -> opcode handlers -> module registration -> full zdebug boot) on both 8.5 (control) and 8.6, and captures an lldb backtrace of the crash. Will be removed once the failure is root-caused. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG --- .github/workflows/debug-arm64-php86.yml | 100 ++++++++++++++++++++++++ .probe/l1-core-init.php | 5 ++ .probe/l2-ext-stmt-compile.php | 8 ++ .probe/l3-ext-stmt-handler.php | 15 ++++ .probe/l4-all-hooks.php | 14 ++++ .probe/payload.php | 22 ++++++ 6 files changed, 164 insertions(+) create mode 100644 .github/workflows/debug-arm64-php86.yml create mode 100644 .probe/l1-core-init.php create mode 100644 .probe/l2-ext-stmt-compile.php create mode 100644 .probe/l3-ext-stmt-handler.php create mode 100644 .probe/l4-all-hooks.php create mode 100644 .probe/payload.php diff --git a/.github/workflows/debug-arm64-php86.yml b/.github/workflows/debug-arm64-php86.yml new file mode 100644 index 0000000..b31869a --- /dev/null +++ b/.github/workflows/debug-arm64-php86.yml @@ -0,0 +1,100 @@ +# TEMPORARY diagnostic workflow for the macOS arm64 + PHP 8.6 integration failure +# on PR #24. Manually dispatched only; removed (with .probe/) once the failure is +# root-caused. Layers the debugger's boot sequence one step at a time and captures +# an lldb backtrace of the crash the CI legs only see as "exit code 11". +name: Debug arm64 PHP 8.6 + +on: + workflow_dispatch: + +jobs: + diagnose: + name: Diagnose (PHP ${{ matrix.php }}, macos-latest) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + # 8.5 is the green control; 8.6 is the failing target + php: ['8.5', '8.6'] + env: + HOMEBREW_NO_AUTO_UPDATE: '1' + HOMEBREW_NO_INSTALL_CLEANUP: '1' + PHP_FLAGS: -d ffi.enable=1 -d zend.assertions=1 -d opcache.enable_cli=0 -d opcache.jit=off + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ffi, opcache + ini-values: ffi.enable=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=0, opcache.jit=off, opcache.jit_buffer_size=0 + coverage: none + + - name: Environment report + run: | + uname -m + php -v + php -r 'echo "ZEND_THREAD_SAFE=", var_export(ZEND_THREAD_SAFE, true), " PHP_DEBUG=", PHP_DEBUG, PHP_EOL;' + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: 'L1: Core::init only' + if: always() + run: php $PHP_FLAGS .probe/l1-core-init.php; echo "exit=$?" + + - name: 'L2: + COMPILE_EXTENDED_STMT' + if: always() + run: php $PHP_FLAGS .probe/l2-ext-stmt-compile.php; echo "exit=$?" + + - name: 'L3: + EXT_STMT handler' + if: always() + run: php $PHP_FLAGS .probe/l3-ext-stmt-handler.php; echo "exit=$?" + + - name: 'L4: + RETURN and THROW handlers' + if: always() + run: php $PHP_FLAGS .probe/l4-all-hooks.php; echo "exit=$?" + + - name: 'L5: zdebug module-check fixture (module registration, no IDE)' + if: always() + run: php $PHP_FLAGS tests/Integration/fixtures/module-check.php; echo "exit=$?" + + - name: 'L6: full zdebug boot, unreachable IDE (the failing repro)' + if: always() + env: + ZDEBUG_MODE: debug + ZDEBUG_CLIENT_HOST: 127.0.0.1 + ZDEBUG_CLIENT_PORT: '1' + ZDEBUG_IDEKEY: phpunit + ZDEBUG_CONNECT_TIMEOUT_MS: '200' + run: | + export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" + php $PHP_FLAGS tests/Integration/fixtures/entry.php; echo "exit=$?" + + - name: 'L6 under lldb (backtrace on crash)' + if: always() + env: + ZDEBUG_MODE: debug + ZDEBUG_CLIENT_HOST: 127.0.0.1 + ZDEBUG_CLIENT_PORT: '1' + ZDEBUG_IDEKEY: phpunit + ZDEBUG_CONNECT_TIMEOUT_MS: '200' + run: | + export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" + lldb --batch \ + -o 'run' \ + -k 'thread backtrace all' \ + -k 'register read' \ + -k 'quit' \ + -- "$(which php)" $PHP_FLAGS tests/Integration/fixtures/entry.php || true + + - name: macOS crash reports + if: always() + run: | + ls -la ~/Library/Logs/DiagnosticReports/ 2>/dev/null || true + for f in ~/Library/Logs/DiagnosticReports/php*; do + [ -e "$f" ] || continue + echo "===== $f =====" + head -c 20000 "$f" + done diff --git a/.probe/l1-core-init.php b/.probe/l1-core-init.php new file mode 100644 index 0000000..dd567d8 --- /dev/null +++ b/.probe/l1-core-init.php @@ -0,0 +1,5 @@ +setOptions(Core::$compiler->getOptions() | Compiler::COMPILE_EXTENDED_STMT); +require __DIR__ . '/payload.php'; +echo "L2 DONE\n"; diff --git a/.probe/l3-ext-stmt-handler.php b/.probe/l3-ext-stmt-handler.php new file mode 100644 index 0000000..e63da5a --- /dev/null +++ b/.probe/l3-ext-stmt-handler.php @@ -0,0 +1,15 @@ +setOptions(Core::$compiler->getOptions() | Compiler::COMPILE_EXTENDED_STMT); +$fires = 0; +OpCode::setHandler(OpCode::EXT_STMT, function ($scope) use (&$fires): int { + $fires++; + return Core::ZEND_USER_OPCODE_DISPATCH; +}); +require __DIR__ . '/payload.php'; +echo "EXT_STMT fires: {$fires}\n"; +echo "L3 DONE\n"; diff --git a/.probe/l4-all-hooks.php b/.probe/l4-all-hooks.php new file mode 100644 index 0000000..c73861e --- /dev/null +++ b/.probe/l4-all-hooks.php @@ -0,0 +1,14 @@ +setOptions(Core::$compiler->getOptions() | Compiler::COMPILE_EXTENDED_STMT); +$counts = ['stmt' => 0, 'ret' => 0, 'throw' => 0]; +OpCode::setHandler(OpCode::EXT_STMT, function ($s) use (&$counts): int { $counts['stmt']++; return Core::ZEND_USER_OPCODE_DISPATCH; }); +OpCode::setHandler(OpCode::RETURN, function ($s) use (&$counts): int { $counts['ret']++; return Core::ZEND_USER_OPCODE_DISPATCH; }); +OpCode::setHandler(OpCode::THROW, function ($s) use (&$counts): int { $counts['throw']++; return Core::ZEND_USER_OPCODE_DISPATCH; }); +require __DIR__ . '/payload.php'; +echo "fires: stmt={$counts['stmt']} ret={$counts['ret']} throw={$counts['throw']}\n"; +echo "L4 DONE\n"; diff --git a/.probe/payload.php b/.probe/payload.php new file mode 100644 index 0000000..d1d4228 --- /dev/null +++ b/.probe/payload.php @@ -0,0 +1,22 @@ + 100) { throw new RuntimeException('nope'); } } catch (RuntimeException) {} + return $doubled; + } +} +function probeHelper(int $value): int +{ + return $value + 1; +} +$service = new ProbeService(); +$total = 0; +foreach ([1, 2] as $value) { + $total += $service->handle($value); +} +$total += probeHelper(5); +$total += $service->handle(101); +echo "PAYLOAD TOTAL={$total}\n"; From a1e7404590a9b8513932ed630e6364883e981d70 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 13:09:12 +0000 Subject: [PATCH 3/4] ci: fold the arm64/8.6 diagnostic job into ci.yml workflow_dispatch workflows on a non-default branch cannot be dispatched, so the temporary diagnostic job runs with the PR's CI instead. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG --- .github/workflows/ci.yml | 94 ++++++++++++++++++++++ .github/workflows/debug-arm64-php86.yml | 100 ------------------------ 2 files changed, 94 insertions(+), 100 deletions(-) delete mode 100644 .github/workflows/debug-arm64-php86.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e097871..d4ea3ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,3 +91,97 @@ jobs: - name: Check coding standards run: composer cs:check + + # TEMPORARY diagnostic job for the macOS arm64 + PHP 8.6 integration failure on + # PR #24 (removed, with .probe/, once root-caused). Layers the debugger's boot + # sequence one step at a time on the failing runner, with 8.5 as green control, + # and captures an lldb backtrace of the crash CI only reports as "exit code 11". + diagnose-arm64-php86: + name: Diagnose arm64 (PHP ${{ matrix.php }}) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + php: ['8.5', '8.6'] + env: + HOMEBREW_NO_AUTO_UPDATE: '1' + HOMEBREW_NO_INSTALL_CLEANUP: '1' + PHP_FLAGS: -d ffi.enable=1 -d zend.assertions=1 -d opcache.enable_cli=0 -d opcache.jit=off + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ffi, opcache + ini-values: ffi.enable=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=0, opcache.jit=off, opcache.jit_buffer_size=0 + coverage: none + + - name: Environment report + run: | + uname -m + php -v + php -r 'echo "ZEND_THREAD_SAFE=", var_export(ZEND_THREAD_SAFE, true), " PHP_DEBUG=", PHP_DEBUG, PHP_EOL;' + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: 'L1: Core::init only' + if: always() + run: php $PHP_FLAGS .probe/l1-core-init.php; echo "exit=$?" + + - name: 'L2: + COMPILE_EXTENDED_STMT' + if: always() + run: php $PHP_FLAGS .probe/l2-ext-stmt-compile.php; echo "exit=$?" + + - name: 'L3: + EXT_STMT handler' + if: always() + run: php $PHP_FLAGS .probe/l3-ext-stmt-handler.php; echo "exit=$?" + + - name: 'L4: + RETURN and THROW handlers' + if: always() + run: php $PHP_FLAGS .probe/l4-all-hooks.php; echo "exit=$?" + + - name: 'L5: zdebug module-check fixture (module registration, no IDE)' + if: always() + run: php $PHP_FLAGS tests/Integration/fixtures/module-check.php; echo "exit=$?" + + - name: 'L6: full zdebug boot, unreachable IDE (the failing repro)' + if: always() + env: + ZDEBUG_MODE: debug + ZDEBUG_CLIENT_HOST: 127.0.0.1 + ZDEBUG_CLIENT_PORT: '1' + ZDEBUG_IDEKEY: phpunit + ZDEBUG_CONNECT_TIMEOUT_MS: '200' + run: | + export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" + php $PHP_FLAGS tests/Integration/fixtures/entry.php; echo "exit=$?" + + - name: 'L6 under lldb (backtrace on crash)' + if: always() + env: + ZDEBUG_MODE: debug + ZDEBUG_CLIENT_HOST: 127.0.0.1 + ZDEBUG_CLIENT_PORT: '1' + ZDEBUG_IDEKEY: phpunit + ZDEBUG_CONNECT_TIMEOUT_MS: '200' + run: | + export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" + lldb --batch \ + -o 'run' \ + -k 'thread backtrace all' \ + -k 'register read' \ + -k 'quit' \ + -- "$(which php)" $PHP_FLAGS tests/Integration/fixtures/entry.php || true + + - name: macOS crash reports + if: always() + run: | + ls -la ~/Library/Logs/DiagnosticReports/ 2>/dev/null || true + for f in ~/Library/Logs/DiagnosticReports/php*; do + [ -e "$f" ] || continue + echo "===== $f =====" + head -c 20000 "$f" + done diff --git a/.github/workflows/debug-arm64-php86.yml b/.github/workflows/debug-arm64-php86.yml deleted file mode 100644 index b31869a..0000000 --- a/.github/workflows/debug-arm64-php86.yml +++ /dev/null @@ -1,100 +0,0 @@ -# TEMPORARY diagnostic workflow for the macOS arm64 + PHP 8.6 integration failure -# on PR #24. Manually dispatched only; removed (with .probe/) once the failure is -# root-caused. Layers the debugger's boot sequence one step at a time and captures -# an lldb backtrace of the crash the CI legs only see as "exit code 11". -name: Debug arm64 PHP 8.6 - -on: - workflow_dispatch: - -jobs: - diagnose: - name: Diagnose (PHP ${{ matrix.php }}, macos-latest) - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - # 8.5 is the green control; 8.6 is the failing target - php: ['8.5', '8.6'] - env: - HOMEBREW_NO_AUTO_UPDATE: '1' - HOMEBREW_NO_INSTALL_CLEANUP: '1' - PHP_FLAGS: -d ffi.enable=1 -d zend.assertions=1 -d opcache.enable_cli=0 -d opcache.jit=off - steps: - - uses: actions/checkout@v4 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: ffi, opcache - ini-values: ffi.enable=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=0, opcache.jit=off, opcache.jit_buffer_size=0 - coverage: none - - - name: Environment report - run: | - uname -m - php -v - php -r 'echo "ZEND_THREAD_SAFE=", var_export(ZEND_THREAD_SAFE, true), " PHP_DEBUG=", PHP_DEBUG, PHP_EOL;' - - - name: Install dependencies - uses: ramsey/composer-install@v3 - - - name: 'L1: Core::init only' - if: always() - run: php $PHP_FLAGS .probe/l1-core-init.php; echo "exit=$?" - - - name: 'L2: + COMPILE_EXTENDED_STMT' - if: always() - run: php $PHP_FLAGS .probe/l2-ext-stmt-compile.php; echo "exit=$?" - - - name: 'L3: + EXT_STMT handler' - if: always() - run: php $PHP_FLAGS .probe/l3-ext-stmt-handler.php; echo "exit=$?" - - - name: 'L4: + RETURN and THROW handlers' - if: always() - run: php $PHP_FLAGS .probe/l4-all-hooks.php; echo "exit=$?" - - - name: 'L5: zdebug module-check fixture (module registration, no IDE)' - if: always() - run: php $PHP_FLAGS tests/Integration/fixtures/module-check.php; echo "exit=$?" - - - name: 'L6: full zdebug boot, unreachable IDE (the failing repro)' - if: always() - env: - ZDEBUG_MODE: debug - ZDEBUG_CLIENT_HOST: 127.0.0.1 - ZDEBUG_CLIENT_PORT: '1' - ZDEBUG_IDEKEY: phpunit - ZDEBUG_CONNECT_TIMEOUT_MS: '200' - run: | - export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" - php $PHP_FLAGS tests/Integration/fixtures/entry.php; echo "exit=$?" - - - name: 'L6 under lldb (backtrace on crash)' - if: always() - env: - ZDEBUG_MODE: debug - ZDEBUG_CLIENT_HOST: 127.0.0.1 - ZDEBUG_CLIENT_PORT: '1' - ZDEBUG_IDEKEY: phpunit - ZDEBUG_CONNECT_TIMEOUT_MS: '200' - run: | - export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" - lldb --batch \ - -o 'run' \ - -k 'thread backtrace all' \ - -k 'register read' \ - -k 'quit' \ - -- "$(which php)" $PHP_FLAGS tests/Integration/fixtures/entry.php || true - - - name: macOS crash reports - if: always() - run: | - ls -la ~/Library/Logs/DiagnosticReports/ 2>/dev/null || true - for f in ~/Library/Logs/DiagnosticReports/php*; do - [ -e "$f" ] || continue - echo "===== $f =====" - head -c 20000 "$f" - done From d8ec2f35a8d5ad09934030aa74228c11978d7aa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 13:19:36 +0000 Subject: [PATCH 4/4] ci: mark the macOS arm64 + PHP 8.6 leg experimental, drop the diagnostics The layered diagnostic run (see the removed diagnose job in the previous commits) isolated the arm64 failure to z-engine itself: with COMPILE_EXTENDED_STMT on, the first EXT_STMT dispatch through a user opcode handler already corrupts VM state (a by-ref closure counter reads back as a ZEngine\Core object), and instrumented debuggees segfault in ZEND_DO_UCALL_SPEC_RETVAL_USED_TAILCALL_HANDLER. Apple Silicon clang builds of PHP 8.6 select the new tail-call VM (ZEND_VM_KIND_TAILCALL); x64 clang and gcc keep the hybrid VM, which is why linux and macOS x64 are green on the same PHP source. PHP 8.5 on the same arm64 runner passes every probe. Reported upstream as lisachenko/z-engine#280. The arm64 8.6 leg keeps running as continue-on-error for visibility and becomes required again once the upstream issue is resolved; README/AGENTS note the platform exception. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01U4cKi87mVQ896uSrw592fG --- .github/workflows/ci.yml | 103 +++------------------------------ .probe/l1-core-init.php | 5 -- .probe/l2-ext-stmt-compile.php | 8 --- .probe/l3-ext-stmt-handler.php | 15 ----- .probe/l4-all-hooks.php | 14 ----- .probe/payload.php | 22 ------- AGENTS.md | 2 +- README.md | 4 +- 8 files changed, 11 insertions(+), 162 deletions(-) delete mode 100644 .probe/l1-core-init.php delete mode 100644 .probe/l2-ext-stmt-compile.php delete mode 100644 .probe/l3-ext-stmt-handler.php delete mode 100644 .probe/l4-all-hooks.php delete mode 100644 .probe/payload.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4ea3ea..308658c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ jobs: tests: name: Tests (PHP ${{ matrix.php }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} + # Experimental legs may fail without failing the workflow (see the matrix) + continue-on-error: ${{ matrix.experimental == true }} strategy: fail-fast: false matrix: @@ -25,7 +27,12 @@ jobs: # matrix. macos-latest is arm64, macos-15-intel is x64. - { os: macos-latest, php: '8.4' } - { os: macos-latest, php: '8.5' } - - { os: macos-latest, php: '8.6' } + # EXPERIMENTAL: Apple Silicon clang builds of PHP 8.6 use the new + # tail-call VM (ZEND_VM_KIND_TAILCALL), under which z-engine's user + # opcode handlers corrupt VM state (lisachenko/z-engine#280) - every + # instrumented debuggee crashes. The leg keeps running for visibility + # and turns required again once the upstream issue is resolved. + - { os: macos-latest, php: '8.6', experimental: true } - { os: macos-15-intel, php: '8.4' } - { os: macos-15-intel, php: '8.5' } - { os: macos-15-intel, php: '8.6' } @@ -91,97 +98,3 @@ jobs: - name: Check coding standards run: composer cs:check - - # TEMPORARY diagnostic job for the macOS arm64 + PHP 8.6 integration failure on - # PR #24 (removed, with .probe/, once root-caused). Layers the debugger's boot - # sequence one step at a time on the failing runner, with 8.5 as green control, - # and captures an lldb backtrace of the crash CI only reports as "exit code 11". - diagnose-arm64-php86: - name: Diagnose arm64 (PHP ${{ matrix.php }}) - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - php: ['8.5', '8.6'] - env: - HOMEBREW_NO_AUTO_UPDATE: '1' - HOMEBREW_NO_INSTALL_CLEANUP: '1' - PHP_FLAGS: -d ffi.enable=1 -d zend.assertions=1 -d opcache.enable_cli=0 -d opcache.jit=off - steps: - - uses: actions/checkout@v4 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: ffi, opcache - ini-values: ffi.enable=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=0, opcache.jit=off, opcache.jit_buffer_size=0 - coverage: none - - - name: Environment report - run: | - uname -m - php -v - php -r 'echo "ZEND_THREAD_SAFE=", var_export(ZEND_THREAD_SAFE, true), " PHP_DEBUG=", PHP_DEBUG, PHP_EOL;' - - - name: Install dependencies - uses: ramsey/composer-install@v3 - - - name: 'L1: Core::init only' - if: always() - run: php $PHP_FLAGS .probe/l1-core-init.php; echo "exit=$?" - - - name: 'L2: + COMPILE_EXTENDED_STMT' - if: always() - run: php $PHP_FLAGS .probe/l2-ext-stmt-compile.php; echo "exit=$?" - - - name: 'L3: + EXT_STMT handler' - if: always() - run: php $PHP_FLAGS .probe/l3-ext-stmt-handler.php; echo "exit=$?" - - - name: 'L4: + RETURN and THROW handlers' - if: always() - run: php $PHP_FLAGS .probe/l4-all-hooks.php; echo "exit=$?" - - - name: 'L5: zdebug module-check fixture (module registration, no IDE)' - if: always() - run: php $PHP_FLAGS tests/Integration/fixtures/module-check.php; echo "exit=$?" - - - name: 'L6: full zdebug boot, unreachable IDE (the failing repro)' - if: always() - env: - ZDEBUG_MODE: debug - ZDEBUG_CLIENT_HOST: 127.0.0.1 - ZDEBUG_CLIENT_PORT: '1' - ZDEBUG_IDEKEY: phpunit - ZDEBUG_CONNECT_TIMEOUT_MS: '200' - run: | - export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" - php $PHP_FLAGS tests/Integration/fixtures/entry.php; echo "exit=$?" - - - name: 'L6 under lldb (backtrace on crash)' - if: always() - env: - ZDEBUG_MODE: debug - ZDEBUG_CLIENT_HOST: 127.0.0.1 - ZDEBUG_CLIENT_PORT: '1' - ZDEBUG_IDEKEY: phpunit - ZDEBUG_CONNECT_TIMEOUT_MS: '200' - run: | - export ZDEBUG_PATH_FILTER="$PWD/tests/Integration/fixtures" - lldb --batch \ - -o 'run' \ - -k 'thread backtrace all' \ - -k 'register read' \ - -k 'quit' \ - -- "$(which php)" $PHP_FLAGS tests/Integration/fixtures/entry.php || true - - - name: macOS crash reports - if: always() - run: | - ls -la ~/Library/Logs/DiagnosticReports/ 2>/dev/null || true - for f in ~/Library/Logs/DiagnosticReports/php*; do - [ -e "$f" ] || continue - echo "===== $f =====" - head -c 20000 "$f" - done diff --git a/.probe/l1-core-init.php b/.probe/l1-core-init.php deleted file mode 100644 index dd567d8..0000000 --- a/.probe/l1-core-init.php +++ /dev/null @@ -1,5 +0,0 @@ -setOptions(Core::$compiler->getOptions() | Compiler::COMPILE_EXTENDED_STMT); -require __DIR__ . '/payload.php'; -echo "L2 DONE\n"; diff --git a/.probe/l3-ext-stmt-handler.php b/.probe/l3-ext-stmt-handler.php deleted file mode 100644 index e63da5a..0000000 --- a/.probe/l3-ext-stmt-handler.php +++ /dev/null @@ -1,15 +0,0 @@ -setOptions(Core::$compiler->getOptions() | Compiler::COMPILE_EXTENDED_STMT); -$fires = 0; -OpCode::setHandler(OpCode::EXT_STMT, function ($scope) use (&$fires): int { - $fires++; - return Core::ZEND_USER_OPCODE_DISPATCH; -}); -require __DIR__ . '/payload.php'; -echo "EXT_STMT fires: {$fires}\n"; -echo "L3 DONE\n"; diff --git a/.probe/l4-all-hooks.php b/.probe/l4-all-hooks.php deleted file mode 100644 index c73861e..0000000 --- a/.probe/l4-all-hooks.php +++ /dev/null @@ -1,14 +0,0 @@ -setOptions(Core::$compiler->getOptions() | Compiler::COMPILE_EXTENDED_STMT); -$counts = ['stmt' => 0, 'ret' => 0, 'throw' => 0]; -OpCode::setHandler(OpCode::EXT_STMT, function ($s) use (&$counts): int { $counts['stmt']++; return Core::ZEND_USER_OPCODE_DISPATCH; }); -OpCode::setHandler(OpCode::RETURN, function ($s) use (&$counts): int { $counts['ret']++; return Core::ZEND_USER_OPCODE_DISPATCH; }); -OpCode::setHandler(OpCode::THROW, function ($s) use (&$counts): int { $counts['throw']++; return Core::ZEND_USER_OPCODE_DISPATCH; }); -require __DIR__ . '/payload.php'; -echo "fires: stmt={$counts['stmt']} ret={$counts['ret']} throw={$counts['throw']}\n"; -echo "L4 DONE\n"; diff --git a/.probe/payload.php b/.probe/payload.php deleted file mode 100644 index d1d4228..0000000 --- a/.probe/payload.php +++ /dev/null @@ -1,22 +0,0 @@ - 100) { throw new RuntimeException('nope'); } } catch (RuntimeException) {} - return $doubled; - } -} -function probeHelper(int $value): int -{ - return $value + 1; -} -$service = new ProbeService(); -$total = 0; -foreach ([1, 2] as $value) { - $total += $service->handle($value); -} -$total += probeHelper(5); -$total += $service->handle(101); -echo "PAYLOAD TOTAL={$total}\n"; diff --git a/AGENTS.md b/AGENTS.md index da06867..c2b7c89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ Composer resolves the matching one for the running PHP. ## Environment requirements -- PHP `^8.4` (8.4, 8.5 and the 8.6 beta are tested in CI), **NTS, linux-x64 or darwin-x64/arm64** (the platforms z-engine ships definitions for). +- PHP `^8.4` (8.4, 8.5 and the 8.6 beta are tested in CI), **NTS, linux-x64 or darwin-x64/arm64** (the platforms z-engine ships definitions for). Exception: 8.6 on darwin-arm64 is broken by PHP 8.6's tail-call VM (lisachenko/z-engine#280) — that CI leg is experimental until the upstream issue is resolved. - `ffi.enable=1` and **JIT off** (`opcache.jit=off`) — the JIT rewrites the executor internals the statement hook plugs into. Both must come from `php.ini`/`-d`. - The debuggee's code must be compiled **after** the debugger initializes; the diff --git a/README.md b/README.md index a408cc6..b418aab 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,13 @@ Precedence, lowest to highest: built-in defaults → Xdebug ini/env → `ZDEBUG_ ## Requirements -- PHP **8.4, 8.5 or 8.6 (beta)** (supported in parallel), **NTS**, **linux-x64 or macOS x64/arm64** (platforms z-engine ships definitions for); Composer resolves the matching z-engine line per minor +- PHP **8.4, 8.5 or 8.6 (beta)** (supported in parallel), **NTS**, **linux-x64 or macOS x64/arm64** (platforms z-engine ships definitions for); Composer resolves the matching z-engine line per minor. On 8.6, macOS **arm64** is not usable yet: Apple Silicon clang builds select PHP 8.6's new tail-call VM, which breaks z-engine's engine hooks ([lisachenko/z-engine#280](https://github.com/lisachenko/z-engine/issues/280)) - `ffi.enable=1` and **`opcache.jit=off`** (the JIT rewrites the executor internals the hook plugs into) - Your app's code must load **after** ZDebug attaches — `auto_prepend_file` guarantees this ## Status -The debugger is complete enough to use as one: an IDE attaches, sets line, conditional, call, return and exception breakpoints, steps over/into/out, walks the stack, reads and writes variables, evaluates expressions, fetches source and detaches — all of it proven end-to-end by a test suite that plays a fake IDE against a real spawned child process, on PHP 8.4, 8.5 and 8.6 (beta) across linux-x64 and macOS (x64 + arm64). +The debugger is complete enough to use as one: an IDE attaches, sets line, conditional, call, return and exception breakpoints, steps over/into/out, walks the stack, reads and writes variables, evaluates expressions, fetches source and detaches — all of it proven end-to-end by a test suite that plays a fake IDE against a real spawned child process, on PHP 8.4 and 8.5 across linux-x64 and macOS (x64 + arm64), and on the PHP 8.6 beta on linux-x64 and macOS x64 (macOS arm64 awaits lisachenko/z-engine#280). What is left is a short list, and most of it is the engine's doing rather than unfinished plumbing: