feat(system): debugger-grade primitives — named frame locals, error/interrupt hooks, self-debugging research - #138
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
CDatain new public surfaces).What's included
Named local variables (the
context_getprimitive)FunctionLikeTrait::getVariableNames()— CV slot names fromop_array->varsvia a typedStructArrayview (slot numbering matchesgetCallVariableByNumber()).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 fixedThe 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 returnsnullinstead 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 reachset_error_handler()— withproceed()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 withEG(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 theOpCode::THROWhandler route (fires pre-throw).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 viaCOMPILE_EXTENDED_STMT+EXT_STMThandler (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).cs:check: clean.Targets
8.4per the support policy; merge-up tomastershould regenerate the 8.5 headers on the target branch rather than merginginclude/textually (the three new symbols +ZEND_CALL_HAS_SYMBOL_TABLEintools/generator/symbols.phpcarry over as-is).Refs #106
🤖 Generated with Claude Code
https://claude.ai/code/session_01M1Cp8nVraSrjYe3NvKP7af
Generated by Claude Code