PHP, reimplementato da zero in Rust. Un runtime PHP 8.5 moderno, memory-safe e predisposto all'asincronia — guidato dal comportamento osservabile, non dall'architettura interna dello Zend Engine.
phpr script.php # un drop-in di `php`, ma è Rust fino in fondoLo Zend Engine — il cuore di PHP — è ~280.000 righe di C accumulate dal 1999. Porta con sé gestione manuale della memoria, un garbage collector custom, un layer di thread-safety (TSRM), una VM generata da macro e un JIT contorto. È solido ma fragile: intere classi di vulnerabilità (use-after-free, buffer overflow) vivono lì per costruzione.
L'intuizione del progetto è ribaltare il problema:
Il contratto da preservare non è il design di Zend, ma l'output osservabile di PHP.
E quell'output ha già un oracolo perfetto: i ~21.500 test ufficiali .phpt del sorgente PHP.
Qualunque runtime che produce lo stesso identico output è PHP. Questo trasforma il lavoro da
«traduzione del C» a «reimplementazione guidata dalla specifica», dove il C si legge solo
per inchiodare la semantica nei casi ambigui.
Il risultato è un engine in cui Rust fa il lavoro pesante a costo zero: l'ownership sostituisce
zend_alloc, Rc+copy-on-write sostituiscono il refcounting manuale, Send/Sync rendono
il multi-threading una proprietà del tipo invece di un sottosistema (TSRM), e un processo
residente rende l'engine async-ready per costruzione.
Un runtime PHP che sia, nell'ordine:
- Fedele — bug-for-bug compatibile con PHP 8.5 sul corpus ufficiale
.phpt(incluse le idiosincrasie del type juggling, i warning legacy, lo stack trace byte-identico). - Sicuro — niente segfault a livello di core; le classi di bug della memoria del C eliminate dal type system di Rust.
- Moderno — distribuibile come singolo binario (l'effetto Go/Deno), con web server nativo integrato e una base nativamente asincrona e multi-thread — superando il limite storico shared-nothing / single-threaded di PHP.
Il banco di prova non è un microbenchmark: è far girare Composer e poi far rispondere una rotta Hello World di Laravel/Symfony. Quei traguardi stressano OOP, autoloading e Reflection più di qualsiasi test sintetico.
| Fase | Traguardo | Stato |
|---|---|---|
| 1. Nucleo semantico | Type juggling fedele all'oracle (zend_operators.c), ==/===, coercizioni |
✅ Fatto |
| 2. Linguaggio completo | Espressioni, control-flow, funzioni, array, reference, closure | ✅ Fatto |
| 3. OOP | Classi, ereditarietà, visibility, static/LSB, magic methods, enum, trait, interfacce |
✅ Fatto |
| 4. Eccezioni & errori | try/catch/finally, engine error catchabili, stack trace, line tracking |
✅ Fatto |
| 5. VM a bytecode | Generatori, yield from, Fiber su frame espliciti — niente unsafe, niente coroutine stackful |
✅ Fatto |
| 6. Memoria | Cycle collector per i riferimenti circolari (l'altro grande «drago») | ✅ Fatto |
| 7. Libreria standard | ~243 builtin: array/string/math/json/preg/mbstring/hash/file/stream… | 🔄 In corso |
| 8. Composer reale | composer install che risolve dipendenze senza crashare |
🔄 In corso |
| 9. Framework bootstrap | Hello World su Laravel / Symfony | ⏳ Prossimo |
| 10. Async & single-binary | Event loop Tokio + web server Axum residente, distribuzione standalone | ⏳ Futuro |
| 11. JIT (Tier 3) | Bytecode pulito → Cranelift/LLVM per il codice macchina al volo | 🔭 Visione |
Un solo motore di produzione: una VM a bytecode. Il sorgente passa per
parser (mago) → AST → HIR → bytecode → VM dispatch loop. (Il progetto è nato con un
tree-walker, poi rimosso una volta che la VM ha raggiunto la piena parità: vedi
HISTORY.md.)
php-rust/crates/
php-types Zval / PhpStr / PhpArray / Object + operatori (l'anima di PHP:
type juggling full-port da zend_operators.c). Zero dipendenze interne.
php-runtime HIR + lowering da `mago`, e la VM a bytecode:
compile.rs (HIR→bytecode) + vm/{mod,exceptions,coroutines,arrays,oop,calls}.rs
php-builtins registry di ~243 builtin (var_dump, array_*, sprintf, json_*, preg_*,
mb_*, hash/encoding, file/stream, …)
php-cli binario `phpr` — drop-in di `php`, stream CLI-faithful + exit code fedele
php-server web server nativo (Axum + Tokio) — la testa di ponte verso l'async
phpt-runner esegue i `.phpt` ufficiali con capability-scan e diff unificato vs oracle
diary/ diario metodologico: 00-reconnaissance … 99-conclusions + metriche
Perché Rust collassa Zend — il payoff strutturale, in cifre:
| Sottosistema Zend | LOC C | Sostituto Rust | LOC Rust |
|---|---|---|---|
VM generata + zend_execute.c |
~146.000 | VM a bytecode (motore unico) | ~9.500 |
zend_compile.c (AST→opcodes) |
~12.400 | lowering AST→HIR | ~1–2.000 |
| lexer re2c + parser Bison + AST | ~25.000 | dipendenza mago + bridge |
~500 |
zend_alloc / zend_gc / TSRM / opcache / win32 |
~88.000 | ownership, Rc+COW, Send/Sync |
~0 |
zend_operators.c (type juggling) |
~3.900 | full-port fedele | ~1.500 |
~280K LOC di C core → ~8–10K LOC di Rust.
Il linguaggio core è completo e fedele: tutto il control-flow, le funzioni, gli array, il
sistema di reference, le closure, l'OOP completo (classi, ereditarietà, visibility, static
- late-static-binding, magic methods, enum, trait, Reflection), le eccezioni (incluso stack
trace byte-identico e gli engine error catchabili), i generatori e i Fiber — questi ultimi
implementati parcheggiando i frame su uno stack esplicito della VM, senza
unsafee senza coroutine stackful, il payoff progettuale del passaggio alla VM a bytecode.
Due dei tre «draghi» storici di un porting PHP sono già stati domati:
- 🐉 Riferimenti circolari → un cycle collector stile Zend (algoritmo possible-roots), con sweep O(candidati): un test patologico da 87.380 oggetti ciclici è passato da ~11s a ~0,25s.
- 🐉 Bug-for-bug compatibility → l'intera strategia è ancorata al corpus
.phpt, l'unico vero scudo contro le regressioni.
Il terzo drago — l'ecosistema di estensioni C (PECL) — resta la sfida aperta di lungo termine: richiederà o un layer FFI di compatibilità o la riscrittura nativa in Rust delle estensioni chiave.
Fedeltà, oggi: differential type-juggling vs PHP reale a 0 mismatch; suite di test interna
verde; corpus ufficiale (~21.500 .phpt) eseguito in continuo come oracolo, con la lista dei
fallimenti tracciata e in calo a ogni sessione. La catena include → autoload → Composer gira
end-to-end oracle-identica su scenari reali; molti comandi composer producono già output
identico all'oracolo.
Lo storico dettagliato dei ~70 step di costruzione è in HISTORY.md; il diario metodologico replicabile è in diary/.
- Composer reale, fino in fondo — chiudere
composer installsu un progetto vero senza crash. È il primo banco di prova che cattura l'attenzione della community. - Robustezza — convertire gli
unwrap/expectraggiungibili da input utente in errori VM tipizzati + fuzzing della pipelinelower/compile, per una garanzia no-panic. - Coda lunga della stdlib — l'ultimo 20% di builtin e comportamenti oscuri (date complesse, angoli di mbstring) che, come in ogni porting, costa l'80% dello sforzo.
- Framework bootstrap — Hello World su Laravel/Symfony: lo stress-test definitivo per autoloading e Reflection.
- Salto async — integrare un event loop Tokio e consolidare
php-server(Axum) in un runtime residente, verso un PHP nativamente concorrente e un singolo binario distribuibile.
cd php-rust
cargo run -p php-cli -- script.php # esegui uno script con `phpr`
cargo test # unit + integration test
# Differential vs oracle (richiede un binario php; si auto-salta se assente):
PHP_ORACLE=/path/to/php cargo test -p php-types --test differential
# Esegui il corpus ufficiale .phpt attraverso la 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> # un test = un sotto-processo, con diffDiagnostica: PHP_RUST_TRACE=hir|body|exec|all phpr script.php mostra su stderr l'HIR
abbassato e/o la traccia d'esecuzione, senza inquinare lo stdout confrontato con l'oracolo.
L'idea «riscrivere PHP in Rust per renderlo asincrono e safe» è un magnete per la community Rust.
Il modo migliore di contribuire una volta presa confidenza: prendere un builtin mancante o un
gruppo di .phpt che falliscono (phpt-runner --list-fails), riprodurli contro l'oracolo, e
chiudere il gap restando byte-identici. La regola d'oro del progetto: l'oracolo ha sempre ragione.
MIT.