Thank you for your interest in contributing to php.rs, a ground-up rewrite of the PHP 8.6 interpreter in Rust. This document covers everything you need to get started.
- Rust 1.70+ (2021 edition) -- install via rustup
- Git -- to clone the repository and the PHP reference source
No other system dependencies are required. There is no need for autoconf, bison, re2c, or any C libraries.
git clone https://github.com/alexisbouchez/php-rs.git
cd php-rs
cargo build
cargo test # verify everything worksThe CLI binary is built at ./target/debug/php-rs-sapi-cli (or ./target/release/php-rs-sapi-cli with --release).
php.rs follows strict Test-Driven Development (TDD) with a Red-Green-Refactor cycle:
- Red -- Write a failing test that captures the exact PHP behavior. Tests are often derived from
.phptfiles inphp-src/tests/. - Green -- Implement the minimum code required to make the test pass.
- Refactor -- Clean up the implementation while keeping all tests green.
cargo build # build everything
cargo build -p php-rs-vm # build a specific crate
cargo test # run all tests
cargo test -p php-rs-types # test a specific crate
cargo test -p php-rs-parser test_if_else # run a single test by name
cargo test -- --nocapture # verbose output
cargo check # check without building
cargo clippy --all-targets --all-features # lint
cargo fmt --all # format
cargo bench -p php-rs-vm # benchmarksThe reference PHP test suite (php-src/tests/) contains 21,000+ test files:
cargo test -p php-rs --test phpt_runner # all PHPT tests
PHPT_DIR=php-src/tests/lang cargo test -p php-rs --test phpt_runner # specific directory- Public types mirroring PHP internals use a
Zprefix:ZVal,ZString,ZArray,ZObject,ZOp,ZOpArray. - Opcode handlers are individual functions:
fn op_add(frame: &mut Frame) -> VmResult. - PHP error levels map to a Rust enum:
E_NOTICE,E_WARNING,E_ERROR,E_PARSE, etc. - Prefer Rust
enumfor PHP types -- they map naturally to PHP's tagged union zvals.
- Unit tests go in
#[cfg(test)]modules within the same file, not in separate test files. - Integration tests (PHPT-based) go in the
tests/directory.
No unsafe block without an accompanying // SAFETY: comment explaining the invariant:
// SAFETY: `ptr` is non-null, properly aligned, and its lifetime
// is bounded by the arena allocator's request scope.
unsafe { &*ptr }All code must pass these checks with no warnings before submission:
cargo fmt --all
cargo clippy --all-targets --all-featuresUse descriptive branch names with a prefix: feat/, fix/, refactor/, or test/.
Examples: feat/array-splice-function, fix/float-coercion-negative-zero, test/phpt-match-expressions.
Use the imperative mood with a type prefix:
feat: implement array_splice() with all overloadsfix: correct float-to-int coercion for negative zerotest: add PHPT-derived tests for match expressions
- Failing tests first. Every PR should include tests that demonstrate the behavior being added or fixed.
- Minimal implementation. Only the code necessary to make the tests pass.
- All checks passing.
cargo test,cargo fmt --all -- --check, andcargo clippymust all succeed. - A clear description of what the PR does and which PHP behavior it targets.
The reference PHP test suite lives at php-src/tests/ and php-src/ext/*/tests/. Tests use the .phpt format:
--TEST--
Description of the test
--FILE--
<?php
echo 1 + 2;
?>
--EXPECT--
3
Key sections: --TEST-- (description), --FILE-- (PHP code), --EXPECT-- (exact output), --EXPECTF-- (output with format specifiers like %s, %d), --SKIPIF-- (skip condition), --INI-- (settings), --ENV--, --ARGS--.
To translate a .phpt file into a Rust test: write a test that compiles and executes the same PHP code and asserts the same output. Place unit tests in #[cfg(test)] modules within the relevant crate.
When behavior is ambiguous, run the PHP code against the reference interpreter (php-src/sapi/cli/php) and match the output exactly.
Each PHP extension is its own crate under crates/php-rs-ext-*/ (56 total). Every extension implements the PhpExtension trait with four lifecycle hooks:
module_init-- called once when the module is loadedmodule_shutdown-- called once when the module is unloadedrequest_init-- called at the start of each requestrequest_shutdown-- called at the end of each request
- Create a new crate:
crates/php-rs-ext-<name>/ - Implement the
PhpExtensiontrait and register all functions - Write tests derived from
php-src/ext/<name>/tests/ - Wire the extension into the SAPI crates so it loads at startup
All extensions must be pure Rust -- no C FFI bindings to existing PHP extensions. Do not implement deprecated PHP features (e.g., mysql_* functions, each()).
The interpreter follows the same layered architecture as the official PHP implementation:
Source Code --> Lexer --> Parser --> Compiler --> VM --> Output
tokens AST opcodes execute
| Layer | Crate | Purpose |
|---|---|---|
| Types | php-rs-types |
ZVal (16-byte tagged union), ZString, ZArray, ZObject |
| Lexer | php-rs-lexer |
Hand-written tokenizer (re2c-equivalent) |
| Parser | php-rs-parser |
Recursive descent parser producing AST |
| Compiler | php-rs-compiler |
AST to opcode array compilation |
| VM | php-rs-vm |
Virtual machine executor with 212 opcodes |
| GC | php-rs-gc |
Garbage collector with cycle detection |
| Runtime | php-rs-runtime |
INI system, output buffering, error handling, streams |
| Extensions | php-rs-ext-* |
One crate per extension (standard, json, pcre, curl, etc.) |
| SAPIs | php-rs-sapi-* |
Server API bindings (CLI, FPM, Embed, WASM) |
| What | Where |
|---|---|
| Type system (zval layout) | php-src/Zend/zend_types.h |
| VM opcodes (212) | php-src/Zend/zend_vm_opcodes.h |
| VM handler definitions | php-src/Zend/zend_vm_def.h |
| Compiler (AST to opcodes) | php-src/Zend/zend_compile.c |
| Parser grammar | php-src/Zend/zend_language_parser.y |
| Lexer definition | php-src/Zend/zend_language_scanner.l |
| Standard library functions | php-src/ext/standard/basic_functions.c |
Please include: (1) the PHP code that produces incorrect output, (2) expected output from the reference PHP 8.6 interpreter, (3) actual output from php.rs, (4) your Rust toolchain version (rustc --version), and (5) your OS and platform.
Feature requests are welcome. If requesting support for a specific PHP function or behavior, include a link to the PHP documentation and, if possible, the corresponding .phpt test files from php-src/.
By contributing to php.rs, you agree that your contributions will be licensed under the MIT License.