Skip to content

feat(system): debugger-grade primitives — named frame locals, error/interrupt hooks, self-debugging research - #138

Merged
lisachenko merged 4 commits into
8.4from
claude/xdebug-z-engine-php-heop18
Aug 7, 2026
Merged

feat(system): debugger-grade primitives — named frame locals, error/interrupt hooks, self-debugging research#138
lisachenko merged 4 commits into
8.4from
claude/xdebug-z-engine-php-heop18

Conversation

@lisachenko

Copy link
Copy Markdown
Owner

Answers the research question "can an Xdebug equivalent be built with z-engine in pure PHP, for self-debugging?"yes for breakpoints/stepping/stack-and-variable inspection, with the analysis in docs/self-debugging.md and the missing core primitives shipped here. The debugger itself is expected to live in a separate repository; this PR provides only the API/core-primitive layer, all wrapped in the usual OO shapes (no CData in new public surfaces).

What's included

Named local variables (the context_get primitive)

  • FunctionLikeTrait::getVariableNames() — CV slot names from op_array->vars via a typed StructArray view (slot numbering matches getCallVariableByNumber()).
  • ExecutionData::getLocalVariables() / getLocalVariable($name) — live frame variables by name, straight from the CV slots, no symbol table needed. IS_UNDEF slots are skipped by the list reader and observable through the by-name reader.

getSymbolTable() segfault fixed

The frame symbol table exists only when the engine materialized one and flagged it with ZEND_CALL_HAS_SYMBOL_TABLE (newly exported constant); the accessor now checks the flag and returns null instead of dereferencing stale garbage. The previously-incomplete test is replaced with coverage of both the null path and a live rebuilt table (IS_INDIRECT entries resolved).

New engine hooks (generator manifest + regenerated 8.4 headers)

  • Core::setErrorCallbackHandler()ErrorCallbackHook (zend_error_cb): observes every engine diagnostic — including @/error_reporting-suppressed ones that never reach set_error_handler() — with proceed() chaining to the engine default.
  • Core::setInterruptHandler()InterruptHook + Executor::requestInterrupt() (zend_interrupt_function / EG(vm_interrupt)): the engine's asynchronous "break" primitive; verified to fire at the next VM interrupt check inside the interrupted frame and to chain correctly to ext/pcntl.
  • zend_throw_exception_hook: symbol only, no wrapper — deliberately. Verified live: the engine fires it with EG(exception) already set and ext/ffi aborts the trampoline ("Throwing from FFI callbacks is not allowed"), destroying the script's catch semantics — same root cause as the observer end-handler finding in feat(system): zend_observer fcall bridge with verified per-function attach path #106. A sacrificial-child test pins the behavior so a future ext-ffi change surfaces as a red test. First-chance exception interception remains the OpCode::THROW handler route (fires pre-throw).
  • The header emitter learned to place declarator names inside non-typedef'd function-pointer types (extern void (*zend_error_cb)(...)); byte-identical pre-check against the committed 8.4 artifacts passed before the manifest change, delta is purely additive, layout guard green.

Research document

docs/self-debugging.md — the feasibility map: statement-level stepping via COMPILE_EXTENDED_STMT + EXT_STMT handler (verified live incl. per-statement named locals), breakpoint/step-over/into/out semantics, suspension-by-blocking, reentrancy rules, the compile-order coverage bound, closed routes (observer API per #106, throw hook per this PR's experiment), DBGp-over-DAP recommendation, limitations table and roadmap. README gets a short tour section.

Verification

  • composer test: 406 tests / 3852 assertions, green (4 skipped + 4 incomplete are pre-existing).
  • --group internal --process-isolation: green on the release build (142 tests).
  • PHPStan level max: clean, baseline untouched. cs:check: clean.
  • Header regeneration per AGENTS.md host recipe (clang + php-config 8.4.19): byte-identical pre-check, additive delta.

Targets 8.4 per the support policy; merge-up to master should regenerate the 8.5 headers on the target branch rather than merging include/ textually (the three new symbols + ZEND_CALL_HAS_SYMBOL_TABLE in tools/generator/symbols.php carry over as-is).

Refs #106

🤖 Generated with Claude Code

https://claude.ai/code/session_01M1Cp8nVraSrjYe3NvKP7af


Generated by Claude Code

claude added 4 commits August 7, 2026 07:05
Adds FunctionLikeTrait::getVariableNames() reading the compiled-variable
name table (op_array->vars) through a typed StructArray view, and pairs
it with the frame CV slots in ExecutionData::getLocalVariables() /
getLocalVariable() so a caller can inspect live frame variables by name
without materializing the (unsafe) symbol table. Declared-but-unset
IS_UNDEF slots are skipped by the list reader and observable through the
by-name reader.

This is a core primitive for external debugger tooling (DBGp context_get
semantics over z-engine frames).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1Cp8nVraSrjYe3NvKP7af
Exports zend_error_cb, zend_interrupt_function and zend_throw_exception_hook
in the generated FFI header (teaching the header emitter to place declarator
names inside non-typedef'd function-pointer types) and wraps the two usable
ones in AbstractHook subclasses:

- Core::setErrorCallbackHandler() -> ErrorCallbackHook: observes every engine
  diagnostic (including error_reporting-suppressed ones) before the userland
  error-handler machinery, with proceed() chaining to the engine default.
- Core::setInterruptHandler() -> InterruptHook + Executor::requestInterrupt():
  the engine's asynchronous break primitive - raise EG(vm_interrupt) and the
  callback fires at the next VM interrupt check inside the interrupted frame.

zend_throw_exception_hook ships as a header symbol only: the engine fires it
with EG(exception) already set and ext/ffi aborts any C-to-PHP trampoline in
that state (same root cause as the observer end-handler finding of PR #106).
A sacrificial-child test pins that behavior so a future ext-ffi change
surfaces; first-chance exception interception remains the THROW opcode route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1Cp8nVraSrjYe3NvKP7af
… flag

The engine materializes a frame symbol table only for tricky cases (variable
variables, extract()/compact()/get_defined_vars(), eval'd code) and flags it
with ZEND_CALL_HAS_SYMBOL_TABLE in the frame's call_info; for every other
frame the symbol_table field is stale garbage and dereferencing it was the
historical segfault behind the incomplete testGetSymbolTable. The accessor
now checks the flag (newly exported into the generated constants) and returns
null for frames without a materialized table, and the test covers both the
null path and reading a live rebuilt table through its IS_INDIRECT entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1Cp8nVraSrjYe3NvKP7af
…PHP)

Records the research behind the new debugger primitives: maps every Xdebug
capability to its z-engine substitute with live-verified status (statement
hook with named locals, error callback, VM interrupt/async break), documents
the engine-closed routes (observer API per PR #106, zend_throw_exception_hook
via the ext-ffi live-exception abort) and the compile-order coverage bound,
sketches breakpoints/stepping/DBGp integration for the external debugger
package, and lists the follow-up roadmap. README gets a short tour section
pointing at the doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M1Cp8nVraSrjYe3NvKP7af
@lisachenko
lisachenko marked this pull request as ready for review August 7, 2026 08:37
@lisachenko
lisachenko merged commit c54079c into 8.4 Aug 7, 2026
5 checks passed
@lisachenko
lisachenko deleted the claude/xdebug-z-engine-php-heop18 branch August 7, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants