Skip to content

francescotinti/php-rust

Repository files navigation

php-rust

Read this in Italiano.

🌐 Official website: phprust.com

📊 Live test coverage & PHP compatibility — parity with the official PHP test suite, area by area.

PHP, reimplemented from scratch in Rust. A modern, memory-safe, async-ready PHP 8.5 runtime — driven by observable behavior, not by the internal architecture of the Zend Engine.

phpr script.php        # a drop-in for `php`, but it's Rust all the way down

💡 The idea

The Zend Engine — the heart of PHP — is ~270,000 lines of C that have piled up since 1999. It carries manual memory management, a custom garbage collector, a thread-safety layer (TSRM), a macro-generated VM, and a convoluted JIT. It's battle-tested but brittle: entire classes of vulnerabilities (use-after-free, buffer overflow) live there by construction.

The insight behind the project is to flip the problem on its head:

The contract worth preserving isn't Zend's design, but PHP's observable output.

And that output already has a perfect oracle: the ~21,500 official .phpt tests shipped with the PHP source. Any runtime that produces the exact same output is PHP. This turns the job from "translating C" into "spec-driven reimplementation", where you only read the C to pin down the semantics in the ambiguous cases.

The result is an engine where Rust does the heavy lifting at zero cost: ownership replaces zend_alloc, Rc+copy-on-write replaces manual refcounting, Send/Sync make multi-threading a property of the type instead of a subsystem (TSRM), and a resident process makes the engine async-ready by construction.


🎯 The goal

A PHP runtime that is, in order:

  1. Faithful — bug-for-bug compatible with PHP 8.5 on the official .phpt corpus (including the quirks of type juggling, legacy warnings, and byte-identical stack traces).
  2. Safe — no segfaults at the core level; the C memory-bug classes eliminated by Rust's type system.
  3. Modern — shippable as a single binary (the Go/Deno effect), with a built-in native web server and a natively async, multi-threaded foundation — moving past PHP's historical shared-nothing / single-threaded limitation.

The real test bench isn't a microbenchmark: it's running Composer and then serving a Hello World route on Laravel/Symfony. Those milestones stress OOP, autoloading, and Reflection far more than any synthetic test.


🗺️ Roadmap

Phase Milestone Status
1. Semantic core Type juggling faithful to the oracle (zend_operators.c), ==/===, coercions ✅ Done
2. Full language Expressions, control flow, functions, arrays, references, closures ✅ Done
3. OOP Classes, inheritance, visibility, static/LSB, magic methods, enums, traits, interfaces ✅ Done
4. Exceptions & errors try/catch/finally, catchable engine errors, stack traces, line tracking ✅ Done
5. Bytecode VM Generators, yield from, Fibers on explicit frames — no unsafe, no stackful coroutines ✅ Done
6. Memory Cycle collector for circular references (the other big "dragon") ✅ Done
7. Standard library 785 registered internal functions (measured): array/string/math/json/preg/mbstring/hash/file/stream/date/session… ✅ Substantial (long tail in progress)
8. Real Composer composer require monolog/monolog end-to-end: resolution, HTTPS download (rustls), unzip, autoload — and the package runs ✅ Done
8b. Real ecosystem PHPUnit 13 green, byte-identical (incl. process isolation); Doctrine DBAL 3769 tests / 0 err / 0 fail on native PDO+sqlite; ORM 3484 tests / 3 err / 13 fail; symfony http-foundation full suite, http-kernel CLOSED: 1663 tests, 0 err / 0 fail; Monolog, collections, inflector, instantiator… ✅/🔄 In progress
9. Framework bootstrap Hello World on Laravel / Symfony ⏳ Next
10. Async & single-binary Tokio event loop + resident Axum web server, standalone distribution ⏳ Future
11. JIT (Tier 3) Clean bytecode → Cranelift/LLVM for on-the-fly machine code 🔭 Vision

🏗️ Architecture

A single production engine: a bytecode VM. Source flows through parser (mago) → AST → HIR → bytecode → VM dispatch loop. (The project started with a tree-walker, later removed once the VM reached full parity: see HISTORY.md.)

php-rust/crates/
  php-types      Zval / PhpStr / PhpArray / Object + operators (the soul of PHP:
                 type juggling, full-port from zend_operators.c). Zero internal dependencies.
  php-runtime    HIR + lowering from `mago`, and the bytecode VM:
                 compile.rs (HIR→bytecode) + vm/{mod,exceptions,coroutines,arrays,oop,calls}.rs
  php-builtins   registry of pure value builtins (var_dump, array_*, sprintf, json_*, preg_*,
                 mb_*, hash/encoding, file/stream, …); together with the VM-side host
                 builtins (reflection, callable, PDO/sqlite, dom/xml, curl, proc_open,
                 session, …) phpr registers 785 internal functions (measured by probe)
  php-cli        the `phpr` binary — drop-in for `php`, CLI-faithful streams + faithful exit code
  php-server     native web server (Axum + Tokio) — the bridgehead toward async
  phpt-runner    runs the official `.phpt` tests with capability scan and unified diff vs oracle
diary/           methodological journal: 00-reconnaissance … 99-conclusions + metrics

Why Rust collapses Zend — the structural payoff, in numbers (all LOC measured with wc -l on PHP 8.5.7 and on this repo, 2026-07-13):

Zend subsystem C LOC Rust replacement Rust LOC
Generated VM + zend_execute.c ~136,000 bytecode VM (single engine): php-runtime/src/vm/ ~37,300
zend_compile.c (AST→opcodes) ~12,400 HIR + lowering + compiler (hir.rs, lower/, compile/) ~15,200
re2c lexer + Bison parser + AST ~23,400 mago dependency; the AST→HIR bridge is counted in the lowering row above
zend_alloc / zend_gc / TSRM / opcache (incl. JIT) / win32 ~108,000 ownership, Rc+COW, Send/Sync + a cycle collector ~1,000
zend_operators.c + numeric strings (type juggling) ~3,900 faithful full-port (ops.rs, convert.rs, numstr.rs) ~1,700

~270K LOC of core Zend C (extensions not counted) → ~67K LOC of engine Rust (php-runtime + php-types) — a ~4:1 engine-vs-engine collapse. The whole project today is ~96K LOC of Rust plus ~5.7K of PHP prelude — and that total also includes the standard library, PDO/sqlite, dom/xml, TLS and the phpt tooling, functionality that on the C side lives in ext//sapi/ and is not part of the 270K.


📍 Where we are

The core language is complete and faithful: all of control flow, functions, arrays, the reference system, closures, full OOP (classes, inheritance, visibility, static + late static binding, magic methods, enums, traits, framework-grade Reflection), exceptions (including byte-identical stack traces and catchable engine errors), generators and Fibers — the latter implemented by parking frames on an explicit VM stack, with no unsafe and no stackful coroutines. The hard parts of modern PHP are here too: PHP 8.4 property hooks and lazy objects (ghost/proxy), first-class callables, strict_types resolved per-unit from the call site.

But the real leap is that the real ecosystem runs:

  • Composer installs packages end-to-end: resolution, native HTTPS download (ureq + rustls), native unzip, autoloader dump — and the installed package executes.
  • PHPUnit 13.2 boots and produces output byte-identical to the oracle.
  • Doctrine DBAL: 3769 tests, 0 errors, 0 failures — on a native Rust implementation of PDO / pdo_sqlite / ext-sqlite3 (bundled rusqlite, with SQLSTATE/errmode/metadata semantics verified one by one against the oracle).
  • Doctrine ORM: 3484 tests, 3 errors / 13 failures (and falling) — hydration, UnitOfWork, XML mapping included. Collections, inflector, lexer, event-manager, instantiator: green.
  • Symfony: http-foundation full suite (0 errors without its server-bound tests), http-kernel CLOSED — the full 1663-test suite at 0 errors / 0 failures, parity with the oracle (DI container compiles, dumps and reloads; real IANA timezones; Zend-faithful destructor timing); String / Console / Process green.
  • Extensions modeled without C: pdo, pdo_sqlite, sqlite3, dom, libxml, simplexml (on the Rust DOM), curl (easy-API on ureq), openssl/TLS (rustls), zip, mbstring, pcre (3 engines), hash, json, pcntl, posix, ctype, session, zlib (system FFI), bcmath, gmp, tokenizer.

Two of the three historical "dragons" of a PHP port have already been tamed:

  • 🐉 Circular references → a Zend-style cycle collector (possible-roots algorithm), with O(candidates) sweep: a pathological test of 87,380 cyclic objects went from ~11s to ~0.25s.
  • 🐉 Bug-for-bug compatibility → the entire strategy is anchored to the .phpt corpus and the real framework suites, the only real shield against regressions.

The third dragon — the C extension ecosystem (PECL) — is being tackled by targeted native rewrites (PDO/sqlite, dom/simplexml, and curl have already fallen that way); a compatibility FFI layer remains the long-term option for the tail.

Fidelity (at HEAD 8018850, 2026-07-14): differential type-juggling vs real PHP at 0 mismatches (37,835 cases — this is the operator differential, a metric distinct from the .phpt corpus); 1,550 green Rust unit/integration tests; on the official Zend/tests corpus 2,493 phpt pass (61.5% of the runnable ones, growing every session, with a "zero pass→fail by name" gate on every commit); ~860 commits of history tracked session by session.

The detailed history of the ~70 build steps lives in HISTORY.md; the replicable methodological journal is in diary/.


🚀 Next steps

  1. WordPress — the new primary target (roadmap reordered: WP before Laravel). symfony/http-kernel is done (0/0) and wp-cli already runs end-to-end from source (wp --info at oracle parity). Next: wp core download + the official SQLite integration plugin (runs on the already-green PDO/SQLite), then a real server SAPI, then mysqli and media (gd/exif/zip). Plan: php-rust/NEXT_SESSION_WORDPRESS.md.
  2. Doctrine ORM to zero — down to 3 errors / 14 failures; the remainder is triaged (XSD schemaValidate, lazy-proxy edges).
  3. Framework bootstrapHello World on Laravel / full Symfony kernel: the ultimate stress test for autoloading and Reflection, within reach now that the DI container runs.
  4. Robustness — convert user-input-reachable unwrap/expect into typed VM errors + fuzz the lower/compile pipeline, for a no-panic guarantee.
  5. The async leap — integrate a Tokio event loop and consolidate php-server (Axum) into a resident runtime, toward a natively concurrent PHP and a shippable single binary.

🛠️ Quickstart

cd php-rust
cargo run -p php-cli -- script.php       # run a script with `phpr`
cargo test                               # unit + integration tests

# Differential vs oracle (requires a php binary; auto-skips if absent):
PHP_ORACLE=/path/to/php cargo test -p php-types --test differential

# Run the official .phpt corpus through the VM:
cargo run -p phpt-runner -- /path/to/php-src/tests /path/to/php-src/Zend/tests
cargo run -p phpt-runner -- --isolate --list-fails <path>   # one test = one sub-process, with diff

Diagnostics: PHP_RUST_TRACE=hir|body|exec|all phpr script.php prints the lowered HIR and/or the execution trace to stderr, without polluting the stdout compared against the oracle.


🤝 Contributing

The idea of "rewriting PHP in Rust to make it async and safe" is a magnet for the Rust community. The best way to contribute once you've found your footing: pick a missing builtin or a group of failing .phpt tests (phpt-runner --list-fails), reproduce them against the oracle, and close the gap while staying byte-identical. The project's golden rule: the oracle is always right.

📄 License

MIT.

About

Modern reimplementation of PHP 8.5 in Rust, driven by observable behavior (.phpt oracle). Faithful port of zend_operators.c with 37835-case differential testing.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors