feat: PHP output-buffering builtins (ob_*) with user handlers, chunk auto-flush, and flags gating#577
Merged
Merged
Conversation
nahime0
force-pushed
the
feat/output-buffering-builtins
branch
from
July 19, 2026 15:44
310a1c9 to
845bab9
Compare
…uting Heap-backed 64-level ob_* buffer stack (__rt_ob_* helpers, both targets), capture branch in __rt_stdout_write/__rt_pr_write, flush-all on every process-exit path, and a register-preserving __rt_vd_write shim so printf, var_dump walkers, readfile, and fpassthru route through the funnel instead of raw write(1) syscalls.
One registry home file per builtin (area Io) with check hooks for the string|false / int|false / array returns and the unsupported-callback rejection on ob_start, a shared lowering module over the __rt_ob_* helpers, and the matching EIR return-type arms.
…r stack New __elephc_eval_ob_* bridge wrappers expose the runtime ob state to the eval interpreter, so static and eval'd code share one buffer stack. RuntimeValueOps gains ob hooks (FFI-backed real impl, Vec-backed FakeOps), 13 core builtin homes wire both dispatch paths, and the names join EVAL_DECLARATIVE_REGISTRY_BUILTINS.
Codegen tests for capture, nesting, flush/clean variants, status queries, buffer growth, script-end and exit() flushing, printf/print_r/var_dump/ readfile/fpassthru capture, case-insensitive calls, and static-eval shared state; error tests for arity and the unsupported ob_start callback; plus an examples/output-buffering demo.
New docs/php/system-and-io.md section, CHANGELOG entry, IO area mapping for the ob_* lowering file, and the regenerated builtin registry/pages (13 new ob_* entries plus line-number drift in existing lowering links).
…or ob_* Handler stubs stored per buffer slot (retained AOT callable descriptors or magician registry ids) run on flush/clean with PHP's phase bits; results map false to pass-through and cast everything else to a persisted string before the argument container is released. Chunked buffers auto-flush at the PHP threshold, cleanable/flushable/removable flags gate operations with PHP's exact notices, ob_get_status reports handler name/type/flags, and the main epilogue drains buffers before teardown so eval-registered handlers still run at script end.
The callback operand resolves to a handler triple by shape: Callable descriptors are retained and named from their descriptor (closure-shaped kinds report Closure::__invoke), runtime strings dispatch through the shared callable descriptor cases (a miss raises PHP's invalid-callback warning and returns false), and boxed Mixed values unbox to those shapes at run time. ob_get_clean/ob_get_flush move to the gating-aware composite helpers, and the check hook now only rejects array-pair callables.
Eval ob_start retains the callable, registers it in a process-local registry (the dynamic-destructor hook pattern), and passes the id through the new __elephc_eval_ob_start_ex bridge; runtime flush points invoke it via the installed hook, which re-enters the interpreter and returns the result cell. Composite get_clean/get_flush, slot metadata, and handler names flow over new bridge labels, scalar callbacks are rejected with PHP's warnings, and freed contexts invalidate their registry entries.
Handler transformation across closure/named/first-class shapes, false pass-through and string casting of returns, raw returns from ob_get_clean/ ob_get_flush, chunk thresholds with WRITE/FINAL phases, per-flag gating with PHP's named notices, unknown-callback warnings, in-handler output discard, status handler fields with started/processed bits, nested handler cascades, script-end handler application, and eval handlers across boundaries; the no-buffer expectations gain PHP's notices and the buffer sizes move to PHP's 16384/page-aligned shape.
system-and-io.md drops the v1 limitations in favor of the full handler, chunk-size, and flags semantics (with the two known divergences), the CHANGELOG bullet covers the complete feature, and the generated builtin pages/registry pick up the updated ob_start declaration.
nahime0
force-pushed
the
feat/output-buffering-builtins
branch
from
July 19, 2026 20:46
845bab9 to
14f7e27
Compare
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.
Implements PHP's complete output-control surface — all 13
ob_*builtins — in both the AOT path and the Magicianeval()runtime, with one shared buffer stack across static and eval'd code.
What's included
Buffer stack (runtime)
chunk_size + 1when chunked).__rt_stdout_write; the remaining rawwrite(1, …)bypasses (printf/vprintf, the var_dump walkers,readfile/fpassthru streaming loops) now route through a register-preserving shim so every stdout writer is captured.
Mixedcallables run on flush/clean with PHP's exact phasebits (
WRITE/CLEAN/FLUSH/FINAL+STARTon first run — values verified against PHP 8).falsereturn passes the raw contents through; any other return is cast to a string.ob_get_clean/ob_get_flushreturn theRAW contents while the handler output is flushed/discarded, like PHP.
ob_start()inside a handler is a fatal error, matching PHP.eval()are invoked from runtime flush points (including static-code flushes and the script-enddrain) through an installed magician hook, mirroring the dynamic-destructor precedent.
PHP-parity semantics
chunk_sizeauto-flushes at the threshold with the WRITE phase.flagsgate operations exactly like PHP (ob_clean← CLEANABLE,ob_flush← FLUSHABLE, end/get ← REMOVABLE), with PHP'sE_NOTICE texts (including the dynamic
… buffer of NAME (LEVEL)forms) routed through the capture funnel so parent buffers seethem.
false.ob_get_status()reports handler name/type/flags (user bit, started/processed bits after the first run), chunk size, and buffersizes;
ob_list_handlers()reports per-level handler names.Known divergences (documented in
docs/php/system-and-io.md)ob_get_status/ob_list_handlers(PHP reportsClosure::__invoke); anonymous closures reportClosure::__invokecorrectly.[$obj, 'method']) are rejected at compile time in AOT (supported in eval).Related
silently corrupting string returns. Pre-existing on
main, independent of this branch; the ob handler tests use typed handlerswhere it matters.
Testing
notices, status/handler introspection, eval↔static interop, script-end/exit drains, print_r/printf/var_dump/readfile/fpassthru
capture).
update-builtin-docs); example inexamples/output-buffering/.