Skip to content

feat: PHP output-buffering builtins (ob_*) with user handlers, chunk auto-flush, and flags gating#577

Merged
nahime0 merged 11 commits into
mainfrom
feat/output-buffering-builtins
Jul 19, 2026
Merged

feat: PHP output-buffering builtins (ob_*) with user handlers, chunk auto-flush, and flags gating#577
nahime0 merged 11 commits into
mainfrom
feat/output-buffering-builtins

Conversation

@nahime0

@nahime0 nahime0 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Implements PHP's complete output-control surface — all 13 ob_* builtins — in both the AOT path and the Magician eval()
runtime, with one shared buffer stack across static and eval'd code.

What's included

Buffer stack (runtime)

  • Heap-backed 64-level buffer stack with doubling growth; capacities follow PHP's shape (16384 default, page-aligned chunk_size + 1 when chunked).
  • Capture hooks into __rt_stdout_write; the remaining raw write(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.
  • Closures, first-class callables, function-name strings, and boxed Mixed callables run on flush/clean with PHP's exact phase
    bits (WRITE/CLEAN/FLUSH/FINAL + START on first run — values verified against PHP 8).
  • false return passes the raw contents through; any other return is cast to a string. ob_get_clean/ob_get_flush return the
    RAW contents while the handler output is flushed/discarded, like PHP.
  • Output produced inside a handler is discarded; ob_start() inside a handler is a fatal error, matching PHP.
  • Handlers registered inside eval() are invoked from runtime flush points (including static-code flushes and the script-end
    drain) through an installed magician hook, mirroring the dynamic-destructor precedent.

PHP-parity semantics

  • chunk_size auto-flushes at the threshold with the WRITE phase.
  • flags gate operations exactly like PHP (ob_clean ← CLEANABLE, ob_flush ← FLUSHABLE, end/get ← REMOVABLE), with PHP's
    E_NOTICE texts (including the dynamic … buffer of NAME (LEVEL) forms) routed through the capture funnel so parent buffers see
    them.
  • Invalid callbacks raise PHP's warning + "Failed to create buffer" notice and return false.
  • ob_get_status() reports handler name/type/flags (user bit, started/processed bits after the first run), chunk size, and buffer
    sizes; ob_list_handlers() reports per-level handler names.

Known divergences (documented in docs/php/system-and-io.md)

  • The first-class callable of a named function reports that function's name in ob_get_status/ob_list_handlers (PHP reports
    Closure::__invoke); anonymous closures report Closure::__invoke correctly.
  • Array-pair callables ([$obj, 'method']) are rejected at compile time in AOT (supported in eval).
  • A builtin name as handler follows the runtime-callable builtin allowlist; PHP fatals on arity instead.

Related

Testing

  • 44 end-to-end codegen tests (capture, nesting, handlers across all callable shapes, chunk thresholds, per-flag gating with
    notices, status/handler introspection, eval↔static interop, script-end/exit drains, print_r/printf/var_dump/readfile/fpassthru
    capture).
  • 8 diagnostics tests, 6 magician interpreter tests, cross-registry parity gates green (signatures identical in both registries).
  • Behavior pinned against real PHP 8 output byte-for-byte where applicable (phases, notices, status fields, chunk buffer sizes).
  • Generated builtin docs/registry regenerated (update-builtin-docs); example in examples/output-buffering/.

@github-actions github-actions Bot added area:builtins Touches PHP builtin declarations or emitters. area:magician Touches eval, include execution, or elephc-magician. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. type:feature Introduces new user-visible behavior or capabilities. labels Jul 19, 2026
@nahime0
nahime0 force-pushed the feat/output-buffering-builtins branch from 310a1c9 to 845bab9 Compare July 19, 2026 15:44
nahime0 added 11 commits July 19, 2026 22:38
…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
nahime0 force-pushed the feat/output-buffering-builtins branch from 845bab9 to 14f7e27 Compare July 19, 2026 20:46
@nahime0
nahime0 merged commit 815bd04 into main Jul 19, 2026
113 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:builtins Touches PHP builtin declarations or emitters. area:magician Touches eval, include execution, or elephc-magician. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant