Nota: questo è il piano iniziale (Fase 0), conservato come artefatto storico. Per lo stato corrente (copertura misurata, rotta di lavoro, tooling) vedi
README.md,php-rust/COVERAGE.mdephp-rust/NEXT_SESSION_WORDPRESS.md; il diario metodologico è indiary/03-translation-log.md.
L'utente vuole migrare PHP (sorgente C in /Volumes/Extreme Pro/Claude/php-8.5.7, ~1,94M LOC) verso Rust. Dopo una prima proposta di porting fedele a Zend (opcode VM), la direzione è cambiata su sua indicazione: non ricreare lo Zend Engine — è un design del 1999-2004 — ma sfruttare Rust e il suo ecosistema per una reimplementazione moderna e semplificata, prima il core del linguaggio, poi le estensioni.
Principio guida: il contratto da preservare non è l'architettura di Zend ma il comportamento osservabile di PHP. L'oracle esiste già: 21.548 test .phpt (95% self-contained) che confrontano lo stdout di uno script. Qualunque runtime che produce lo stesso output è "PHP". Questo trasforma il lavoro da traduzione del C a reimplementazione guidata dalla spec, dove il C si legge solo per chiarire semantica ambigua.
- Nessun runtime PHP completo e maturo esiste in Rust → greenfield per il runtime.
- mago (carthage-software/mago, Apache-2.0, attivo): lexer+parser PHP 8.x maturi → si riusa come front-end, eliminando ~25K LOC di scanner re2c + grammatica Bison.
- php-parser-rs archiviato; php.rs hobby-scale; PHPantom solo LSP.
| Sottosistema Zend | LOC C | Sostituto Rust | LOC Rust stimate |
|---|---|---|---|
zend_vm_def.h + zend_vm_execute.h (generato) + zend_execute.c |
~146.000 | Evaluator tree-walking su HIR (AST risolto), match su enum |
3–5K |
zend_compile.c (AST→opcodes) |
12.400 | Lowering AST→HIR (risoluzione variabili→slot, hoisting funzioni) | 1–2K |
| Lexer re2c + parser Bison + zend_ast | ~25.000 | dipendenza mago + bridge | ~500 |
zend_alloc.c (allocator per-request) |
3.600 | allocator di sistema + ownership/Drop | 0 |
zend_gc.c (GC ciclico) |
2.400 | Rc + COW; cicli possibili solo con &$x/oggetti → differito |
0–300 |
| TSRM (thread safety 2002-style) | 2.000 | Send/Sync del type system |
0 |
Zend/Optimizer + opcache |
~72.000 | irrilevante: processo residente tiene l'HIR in memoria (l'opcache esiste perché PHP-C ri-parsa a ogni richiesta) | 0 |
zend_hash.c + zend_string |
4.500 | PhpArray + PhpStr propri (semantica osservabile, non layout) |
~800 |
zend_operators.c (type juggling) |
3.900 | irriducibile: va portato fedelmente — è l'anima di PHP | ~1.500 |
| win32/ (emulazione POSIX) | 7.500 | std cross-platform |
0 |
~280K LOC del core si riducono a ~8–10K LOC Rust. L'unico modulo che richiede porting fedele riga-per-riga è zend_operators.c (+ formattazione float e messaggi di errore): tutto il resto è sostituito da design moderno o crate.
sorgente PHP ──mago──► AST ──lowering──► HIR (AST risolto: slot variabili,
salti risolti, funzioni hoisted, span per line#)
│
Evaluator (tree-walk, match su enum)
│
php-types: Zval / PhpStr / PhpArray / operators
│
Builtins: trait + registry, implementati su crate Rust
Decisioni chiave (D-decisioni per 02-mapping-table.md):
- Zval = enum (
Null/Bool/Long(i64)/Double(f64)/Str(Rc<PhpStr>)/Array(Rc<PhpArray>)), COW esatto viaRc::make_mut(=SEPARATE_ARRAYdi Zend).Undefper la semantica "undefined variable". - PhpStr = bytes (
Box<[u8]>), maiString: le stringhe PHP sono binarie (vincolo per il differential). - PhpArray proprio (no indexmap): ordered hash con chiavi
Int|Str, canonicalizzazione numeric-string (ZEND_HANDLE_NUMERIC),next_freeche non decresce. ~600 LOC. - Niente bytecode: HIR tree-walk. Escape hatch futuro se serve performance: bytecode leggero o cranelift, dietro la stessa semantica già validata dai test. La correttezza prima, registrata dalla baseline .phpt.
- Riferimenti
&$xdifferiti (=Rc<RefCell<Zval>>quando serviranno); senza riferimenti/oggetti il PHP procedurale non può creare cicli → niente GC. - Esecuzione web moderna: in prospettiva non si porta fpm/cgi — un server residente (axum/hyper) con isolamento per-request (ogni richiesta = contesto nuovo, share-nothing come PHP) sostituisce fpm + opcache by design. Fuori scope Tier 1, ma orienta l'architettura (nessun global mutabile nascosto).
| Estensione | LOC C (incl. bundle) | Strategia Rust |
|---|---|---|
| ext/standard | 74K | Reimplementazione incrementale guidata dalla frequenza nei test; molte funzioni = 10-50 LOC Rust su std |
| ext/pcre (PCRE2) | 182K | crate pcre2 (semantica PCRE esatta) o regex+fancy-regex con fallback — decidere su differential |
| ext/date (timelib) | 112K | jiff/chrono per il calendario + scoped port del solo parser strtotime/format (la parte davvero PHP-specifica) |
| ext/json | 4K | port diretto (~800 LOC, semantica PHP specifica) o serde_json + wrapper |
| ext/hash | 11K | RustCrypto digest (glue ~100 LOC/algoritmo) |
| ext/mbstring (libmbfl) | ~80K | encoding_rs + icu4x |
| ext/openssl, sodium | 18K | rustls + RustCrypto + dryoc |
| ext/curl | 5.5K | reqwest |
| sqlite3/pdo, mysqli/mysqlnd, pgsql | ~36K | rusqlite, sqlx/mysql_async, tokio-postgres |
| zlib/bz2/zstd, fileinfo, gd, intl | ~70K | flate2/bzip2/zstd, infer/tree_magic_mini, image, icu4x |
Il pattern è sempre: crate maturo per il lavoro pesante + strato sottile di fedeltà PHP (firme, type juggling degli argomenti, formato output/errori) validato dai .phpt dell'estensione.
- Tier 1 (questo piano): core procedurale — tipi, operatori, controllo di flusso, array, funzioni utente, builtin essenziali. Oracle:
Zend/tests(~600-900 eleggibili, target 400-500 verdi). - Tier 2: OOP (classi, interfacce, ereditarietà, exceptions, closures) — necessario per "gran parte del codice PHP" reale. Sblocca migliaia di .phpt.
- Tier 3: estensioni core via crate (pcre, date, json, hash, mbstring, ext/standard completo). Oracle:
ext/*/tests. - Tier 4: SAPI web moderno (server residente axum, superglobals, sessions).
| # | Step | Verifica |
|---|---|---|
| 0 | Scaffolding workspace + diary + CI | cargo test verde |
| 1 | php-types: ZStr, Zval, PhpArray (chiavi, next_free, ordine, COW) |
unit test casi limite |
| 2 | Operatori/conversioni (port fedele di zend_operators.c: compare :2306, add :1200, concat :2017, identical :2508, smart_streq :3373, increment :2712) |
differential ~500 espressioni vs php -r 'var_dump(...)' |
| 3 | Bridge mago → HIR (slot variabili, span, hoisting) | smoke test script campione |
| 4 | Evaluator v1: echo, variabili, assegnamenti, if/while/for, ternario, break/continue | differential su corpus script |
| 5 | Builtins registry + nucleo (var_dump formato esatto, strlen, gettype, is_*) + float formatting (echo precision=14 vs var_dump shortest-roundtrip) | differential INF/NAN/-0.0/0.1+0.2 |
| 6 | phpt-runner: parser sezioni, EXPECTF→regex (regole di run-tests.php), capability-scan automatico (class|trait|yield|try|&$ → SKIP motivato), expectations.toml, modalità differential vs php di sistema |
prima baseline su Zend/tests/*.phpt committata |
| 7 | Array end-to-end (accesso/assegnazione dim, isset/empty/unset, nesting COW, var_dump ricorsivo) + foreach + switch/match | Zend/tests/foreach/, baseline ↑ |
| 8 | Funzioni utente (hoisting, parametri/default, ricorsione, return) | differential fib + .phpt |
| 9 | Fedeltà diagnostica: Warning: Undefined variable $x in %s on line %d, Undefined array key, DivisionByZeroError — metà degli EXPECTF li contiene |
.phpt EXPECTF |
| 10 | Espansione builtin per frequenza nei test (implode, count, substr, sprintf-subset, array_keys/values, in_array, print_r) | baseline ↑ |
| 11 | Chiusura Tier 1: sweep completo, classificazione failure A/B/C/D, D-NEW in 04-divergences.md, freeze baseline, conclusions |
report finale |
Commit per step, entry in 03-translation-log.md, check-in con l'utente agli step 2, 6, 9, 11.
php-rust-experiment/
├── CLAUDE.md, EXPERIMENT_PLAN.md
├── php-rust/crates/
│ ├── php-types # Zval/PhpStr/PhpArray + operators (zero dep interne)
│ ├── php-runtime # HIR, lowering da mago, evaluator, errori (unico dep su mago)
│ ├── php-builtins # trait Builtin + funzioni
│ ├── php-cli # binario `phpr file.php` / `-r`
│ └── phpt-runner # harness .phpt + differential
└── diary/ # 00-reconnaissance, 01-semantic-model, 02-mapping-table,
# 03-translation-log, 04-divergences, 99-conclusions, metrics
original/ non si copia (1,9M LOC): si referenzia il repo esistente. Fasi metodologiche legacy-port mantenute ma alleggerite: Fase 1 (semantic model, ~3h, solo comportamento osservabile Tier 1, ≥25 citazioni file:line), Fase 2 (mapping table con le D-decisioni sopra), Fase 3 = gli 11 step, Fase 4 integrata (il differential è nativo: i .phpt SONO la testsuite originale, niente converter), Fase 5 sintesi.
- Performance: tree-walk < opcode VM Zend in throughput. Accettato: Tier 1 misura correttezza; l'architettura residente recupera il costo di parse/compile che PHP-C paga a ogni richiesta, e l'escape hatch bytecode resta aperto.
- PCRE:
regexcrate non è PCRE; se i differential mostrano divergenze, si usapcre2(binding FFI) — fedeltà > purezza Rust. - strtotime/date parsing: nessun crate copre la semantica timelib → scoped port dedicato in Tier 3.
- Test che osservano internals (refcount, opcache, gc_collect_cycles) → skip-list motivata, non sono semantica del linguaggio.
cargo testverde a ogni step.phpt-runner --differential --php $(which php)su Zend/tests: baseline committata, mai in regressione tra step.- Fine Tier 1: sweep + report (verdi/skip/xfail + divergenze D-NEW catalogate).