Generato con assistenza AI (Claude Fable 5). Data: 2026-06-13. Le decisioni D-Gn sono il contratto della Fase 3. Status:
confermatosalvo nota.
| ID | Costrutto C / sottosistema | Scelta Rust | Razionale | Status |
|---|---|---|---|---|
| D-G1 | zval (union+tag, 16B) | enum Zval { Undef, Null, Bool(bool), Long(i64), Double(f64), Str(Rc<PhpStr>), Array(Rc<PhpArray>) } |
ADT nativo; stessa taglia; nessun unsafe; Undef serve per la diagnostica "Undefined variable" |
confermato |
| D-G2 | refcount + COW (SEPARATE_*) |
Rc + Rc::make_mut su ogni scrittura |
semantica di separazione identica, gratis | confermato |
| D-G3 | zend_string | PhpStr { hash: Cell<u64>, bytes: Box<[u8]> }; mai String |
stringhe PHP = byte binari; hash lazy come Zend (non osservabile) | confermato |
| D-G4 | HashTable | PhpArray proprio: Vec<Option<(Key,Zval)>> (tombstone) + HashMap<Key,u32> + next_free: i64 |
indexmap non modella chiavi duali/canonicalizzazione/next_free; semantica osservabile > layout | confermato |
| D-G5 | zend_alloc (3.6K LOC) | allocator di sistema + ownership | per-request pool irrilevante per un processo Rust | confermato |
| D-G6 | zend_gc ciclico (2.4K LOC) | scope-out Tier 1; Rc basta |
senza &$x/oggetti/closure il PHP procedurale non crea cicli (array sono valori COW) |
confermato |
| D-G7 | TSRM | nulla (Send/Sync) | thread-safety by type system | confermato |
| D-G8 | lexer re2c + parser Bison + zend_ast (~25K LOC) | dipendenza mago (Apache-2.0) + bridge isolato in un modulo di php-runtime | Strategia A; parse error message divergenti → skip-list | confermato |
| D-G9 | zend_compile + opcodes + VM generata (~158K LOC) | lowering AST→HIR (slot variabili risolti, funzioni hoisted, span) + evaluator tree-walking match |
decisione utente: comportamento, non architettura; escape hatch bytecode futuro | confermato |
| D-G10 | Optimizer + opcache (~72K LOC) | niente | un processo residente non ri-parsa; rustc ottimizza l'evaluator | confermato |
| D-G11 | zend_operators.c | porting fedele in php-types::ops (~1.5K LOC) |
è l'anima osservabile di PHP; unico modulo tradotto riga-per-riga | confermato |
| D-G12 | double→string | due funzioni: to_str_precision14 (echo) e to_str_shortest (var_dump, via Ryū di format!("{}") con correzioni) |
rischio n.1; differential dedicato | da-validare |
| D-G13 | errori/warning | canale Diag nel contesto di esecuzione, scritti su stdout interleaved col formato main/main.c:1493 |
metà degli EXPECTF li asserta | confermato |
| D-G14 | riferimenti &$x |
slot-level enum Slot { Value(Zval), Ref(Rc<RefCell<Zval>>) } (NON un variant di Zval); promozione on-demand; vedi sezione "Step 11" per le sotto-decisioni D-R* |
ROI: sblocca la famiglia builtin by-ref + by-ref param | in-progress (step 11) |
| D-G15 | exit codes | fatal → 255, exit(n) → n, default 0 |
Zend/zend.c:1625 |
confermato |
| D-G16 | builtin | trait Builtin + registry HashMap<&[u8], fn> in php-runtime; implementazioni in php-builtins |
evita ciclo di dipendenze; espansione incrementale | confermato |
| Modulo C | LOC | Strategia | Note |
|---|---|---|---|
| Zend/zend_operators.c | 3.9K | C — full port (semantico) | unico full port del progetto |
| Zend/zend_hash.c, zend_string.* | 4.5K | D — scoped port | solo semantica osservabile (§3 semantic model) |
| Zend lexer/parser/ast | ~25K | A — adapter (mago) | bridge isolato |
| zend_compile + VM | ~158K | sostituzione architetturale (HIR+evaluator) | non è porting: design nuovo |
| zend_alloc, zend_gc, TSRM, Optimizer, opcache, win32 | ~88K | scope-out totale | sostituiti dal linguaggio/architettura |
| ext/standard (subset) | 74K | F — selective port | funzione per funzione, guidato dalla frequenza nei test |
| ext/pcre, ext/date, ext/json, ext/hash, ext/mbstring… | ~400K | A — adapter su crate (Tier 3) | vedi piano, fuori scope Tier 1 |
Design pass scritto a fine step 10 (Claude Opus 4.8) prima dell'implementazione, radicato nel modello di storage reale dell'evaluator. L'implementazione TDD parte in una sessione dedicata. Le D-R* sono il contratto di quella sessione.
Oggi le variabili vivono in slots: Vec<Zval> (eval.rs:141), un Zval per valore
per slot. Lettura: read_var clona (eval.rs:819). Scrittura: self.slots[slot] = v
(eval.rs:978). Le chiamate utente fanno frame-swap con un Vec<Zval> fresco
(eval.rs:471-478). Gli heap-type (Str/Array) sono già Rc con CoW via Rc::make_mut
(D-G2). L'assegnamento $a = $b è una copia di valore (Rc-clone), semantica PHP corretta.
Il vincolo di ownership (Layer 1): un Vec<Zval> piatto non può esprimere "due slot
condividono lo stesso valore mutabile" — Rust vieta due &mut allo stesso dato. La
reference PHP (zend_reference, IS_REFERENCE) è esattamente aliasing mutabile
condiviso. In un interprete single-thread (D-G7: nessun thread) lo strumento
idiomatico è Rc<RefCell<Zval>> — non Arc<Mutex> — coerente con l'uso di Rc già
presente nel codebase.
+-- Layer 1: aliasing mutabile condiviso (no due &mut su un Vec<Zval>)
| Problema: $b = &$a deve far vedere a entrambi le scritture dell'altro
| ^
+-- Dominio: interprete single-thread, modello Rc+CoW già in uso (m02/m03)
| Vincolo: niente thread (D-G7) -> Rc non Arc; serve interior mutability
| v
+-- Layer 2: scelta di design
Decisione: enum Slot { Value(Zval), Ref(Rc<RefCell<Zval>>) },
promozione lazy (come IS_REFERENCE wrappa solo quando serve)
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-R1 | Rappresentazione | enum Slot { Value(Zval), Ref(Rc<RefCell<Zval>>) }; slots: Vec<Slot>. Il caso comune resta Value (zero overhead, nessun cambio di comportamento sui test esistenti). |
Mirror fedele di Zend: una zval è un valore semplice e diventa IS_REFERENCE solo quando si applica &. Penalizza zero il 99% non-reference. Scartato "tutti gli slot Rc<RefCell>" (overhead su ogni read + rischio di sbagliare value-vs-ref) e scartato Zval::Ref come variant (blast radius enorme: ogni match in ops/convert/var_dump/builtin dovrebbe deref-are). |
| D-R2 | Lettura variabile | Value(z) → z.clone(); Ref(cell) → cell.borrow().clone(). read_var/silent_get/read_place_value acquisiscono un &Zval via un helper slot_value(slot) -> Zval (o with_slot). |
La lettura è sempre per valore (CoW preserva la semantica copy). |
| D-R3 | Scrittura variabile | $x = v: se Value → rimpiazza con Value(v); se Ref(cell) → *cell.borrow_mut() = v (write-through, visibile a tutti gli alias). |
È la semantica PHP cruciale: assegnare a una variabile che è una reference scrive attraverso il legame. |
| D-R4 | Creazione $b = &$a |
Nuovo nodo HIR AssignRef { target: Slot, source: Place } (oggi lower.rs:618 rifiuta l'operatore &). Eval: assicura che lo slot sorgente sia Ref (se Value(z) → promuovi a Ref(Rc::new(RefCell::new(z)))), poi target slot = Ref(Rc::clone(cell)). |
Promozione lazy: la sorgente diventa reference solo qui. |
| D-R5 | unset($b) su reference |
Lo slot torna Value(Undef) (rilascia il suo Rc-clone della cella); gli altri alias mantengono il valore. |
Semantica PHP: unset rompe solo quel legame, non il dato condiviso. Già esiste unset_place (eval.rs:1036). |
| D-R6 | Parametri by-ref f(&$x) |
lower.rs:367 e FnDecl/Param guadagnano by_ref: bool. Il call path (eval.rs:647 Call) per ogni arg by-ref non clona un valore ma lega la cella della variabile-argomento (promuovendola a Ref se serve) nello slot-parametro del callee. argv: Vec<Zval> diventa insufficiente → introdurre enum Arg { Val(Zval), Ref(Rc<RefCell<Zval>>) } (o risolvere gli arg by-ref separatamente prima del frame-swap). Un arg by-ref che non è una variabile (es. literal) → Error PHP "Only variables should be passed by reference" (Notice/Warning, poi passa per valore). |
Sblocca la base per i builtin by-ref. |
| D-R7 | Builtin by-ref (array_push/sort/array_pop/array_shift/str_replace $count) |
Estendere l'ABI builtin (D-G16): una tabella di arity by-ref per builtin (quali posizioni sono &), e una nuova signature o un Ctx arricchito che dà accesso &mut Zval allo slot dell'argomento. Opzione minima: un secondo registry RegistryRef con signature fn(&mut [Arg], &mut Ctx). Da rifinire in implementazione. |
È il driver primario dello step (la famiglia è molto usata nel corpus). |
| D-R8 | Scrittura annidata via reference ($ref[0] = 1) |
write_place/unset_place (eval.rs:976,1036) ottengono &mut Zval dallo slot tramite l'helper di D-R2: per Ref(cell) usano &mut *cell.borrow_mut() passato a write_into. write_into resta invariato (lavora su &mut Zval). |
Riusa tutta la logica CoW/auto-vivify esistente. |
| D-R9 | var_dump / print_r | Le reference a livello di variabile sono trasparenti: si deref-a e si stampa il valore (PHP non annota & per le reference top-level). Nessun cambio a dump/print_r_into. |
Mantiene il blast radius minimo. L'annotazione & compare solo per reference dentro array/oggetti → vedi scope-out. |
Brainstorming (architettura) → decisioni utente: unificare su
Zval::Ref(rimuovereBinding); scope = foreach-by-ref + element-&, defer return-by-ref. Semantiche tutte verificate contro l'oracle/tmp/php-src/sapi/cli/php(foreach-by-ref[1,2,3]→[10,20,30], lingering gotcha1,2,2,$x=&$a[0],$a[0]=&$x,&int(5)in var_dump, ref-collapse, ref-survives-copy).
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-R10 | Rappresentazione (unificata) | Aggiungo Zval::Ref(Rc<RefCell<Zval>>). Invariante: l'interno non è mai un Ref (ref-to-ref collassa; slot_cell riusa la cella esistente). Rimuovo enum Binding: gli slot tornano Vec<Zval>, una variabile-reference contiene Zval::Ref(cell). Helper 11a/b/c rimappati su Zval: slot_clone→deref_clone, slot_set→write-through se Ref, slot_cell→promuove/clona la cella, &mut Zval (IncDec/write_into)→&mut *c.borrow_mut(). |
Fedele a Zend (un solo IS_REFERENCE), rimuove un caso speciale. Scartato additivo (Binding+Zval::Ref) per non avere due rappresentazioni della stessa cosa. |
| D-R11 | Deref-on-read (contenimento) | Nuovo Zval::deref_clone(&self) -> Zval. Un Ref esiste solo come slot/elemento e si dereferenzia appena materializzato. Siti (~9): read_index, snapshot foreach by-value, var_dump, print_r, builtin che leggono valori d'array (implode/in_array/array_values/array_merge/sort). |
ops.rs/convert.rs non cambiano — non ricevono mai un Ref (zero rischio sui 37.835 differential). |
| D-R12 | Element-& assignment | AssignRef { target, source } generalizza Slot→enum { Var(Slot), Elem(Place) } su entrambi i lati. $x=&$a[0]: promuovo l'elemento a Ref(cell) in-place (CoW), lego $x a clone della cella. $a[0]=&$x: scrivo Ref(cella di $x) nel place. lower.rs abbassa entrambi i lati come Place. |
Riusa slot_cell + write_into. |
| D-R13 | foreach-by-ref | StmtKind::Foreach guadagna by_ref: bool; lower accetta & sul value-target. eval: iterabile = variabile con array; snapshot delle chiavi; per ogni chiave promuovo $a[k] a Ref(cell) e lego il loop-var a Ref(clone(cell)). Niente auto-unset → lingering gotcha emerge naturalmente. |
Mutazione propaga alla sorgente; fedele a PHP. |
| D-R14 | var_dump / print_r | var_dump: elemento Ref → prefisso & + deref dell'interno. print_r: deref trasparente (NESSUN &, verificato oracle). Ref top-level restano trasparenti (D-R9). |
Solo var_dump annota le reference dentro container. |
| D-R15 | Cicli | $a[0]=&$a crea un ciclo; Rc<RefCell> lo leak-a. Accettato (D-G6, nessun GC ciclico Tier 1), documentato. |
Coerente con la scelta Rc senza weak/GC. |
Scope-out di 11d: return-by-ref (function &f()), array-literal con elemento-ref ([&$x]), foreach-by-ref su non-lvalue.
Sotto-suddivisione TDD 11d: 11d-1 Zval::Ref + rimozione Binding + deref (refactor a parità di comportamento: i 185 test restano verdi); 11d-2 element-& ($x=&$a[0], $a[0]=&$x); 11d-3 foreach-by-ref (+ lingering gotcha); 11d-4 var_dump & annotation.
Dialogo di design → decisioni utente: fare
global $x+$GLOBALS['literal']insieme (stessa infrastruttura,globalha più valore sul corpus e mappa suZval::Ref), scope nomi statici (defer indici dinamici), meccanismo refactor del frame (overlay globals/locals). Semantiche verificate sull'oracle:globalrw (59),globalcrea global (7),$GLOBALSrw (38),$GLOBALS['n']=5crea nuovo global (5),isset($GLOBALS['z'])indefinito → false senza warning.
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-12.1 | Frame model (refactor) | Sostituire lo swap in blocco di self.slots con globals: Vec<Zval> (sempre il frame globale) + locals: Option<Vec<Zval>> (Some mentre gira una funzione). Accesso via frame()/frame_mut() = locals.as_mut().unwrap_or(&mut globals). Idem names. call_user_fn setta locals = Some(fresh) e ripristina al return. Stesso refactor in lower.rs: tenere global_slots/global_index in campi dedicati + overlay locale durante lower_function (oggi mem::take), così il global index è raggiungibile mentre si abbassa il corpo di una funzione. |
È l'unico modo per raggiungere il frame globale per nome da dentro una funzione. Scartato l'hack "campo aggiuntivo" (due percorsi, debito). I ~13 access-site agli slot (già maneggiati in 11d) passano per frame_mut(). |
| D-12.2 | global $x; |
Nuovo StmtKind::Global(Vec<GlobalBinding>) con GlobalBinding { local: Slot, global: Slot }. Lowering: per ogni var, slot locale (scope corrente) + slot globale (scope globale, pre-registrato se assente). Eval: let cell = make_cell(&mut self.globals[global]); self.frame_mut()[local] = Zval::Ref(Rc::clone(&cell)). A scope globale è un no-op (local == global). |
Riusa interamente Zval::Ref/make_cell di 11d: global $x è un alias-by-reference del globale. |
| D-12.3 | $GLOBALS['literal'] |
Place guadagna base: PlaceBase con enum PlaceBase { Local(Slot), Global(Slot) } (oggi slot: Slot → base). Lettura: nuovo ExprKind::GlobalVar(Slot) (base d'espressione, così $GLOBALS['x'][0] = Index{base: GlobalVar, ..}). Scrittura/compound: il place con base: Global(slot) opera sul frame globals. I siti place (resolve_steps/write_place/read_place_value/silent_get/unset_place) leggono self.frame_for(base) invece di self.slots[slot]. |
$GLOBALS['x'] è la variabile globale x; modellarla come base globale gestisce nested/compound ($GLOBALS['x'][0]+=1) gratis. |
| D-12.4 | Pre-registrazione slot globali | In lowering, incontrando global $x o $GLOBALS['literal'], garantire uno slot nel global index (anche da dentro una funzione). Così un globale referenziato solo via $GLOBALS['n'] (mai come bare $n) ottiene comunque uno slot → $GLOBALS['n']=5 crea il global (oracle 5). |
Evita l'overflow HashMap finché gli indici sono literal. |
| D-12.5 | Diagnostica | $GLOBALS['undef'] in lettura → come una lettura di global indefinito (verificare sull'oracle in implementazione: probabile "Undefined variable" o "Undefined array key"). isset($GLOBALS['z']) → false silenzioso. |
Da rifinire contro l'oracle nel sotto-step 12-3. |
| D-12.6 | Scope-out | $GLOBALS[$dynamic] (indice non-literal), $GLOBALS come array intero (foreach($GLOBALS), passarlo, count($GLOBALS)), globali engine (argc/argv/_SERVER…). |
Richiedono risoluzione nome→slot a runtime + overflow HashMap<Vec<u8>,Zval> per globali non nella slot table. Deferiti. |
Sotto-suddivisione TDD step 12: 12-1 refactor frame overlay globals/locals (parità: i 201 test restano verdi); 12-2 global $x; (ref-based, riusa Zval::Ref) — TDD da $x=5; function f(){global $x; $x=9;} f(); echo $x; → 9; 12-3 $GLOBALS['literal'] read/write/compound + pre-registrazione (Place.base) — TDD da $x=3; function f(){$GLOBALS['x']=8;} f(); echo $x; → 8.
Step 12 IMPLEMENTATO (sessione 2026-06-14, TDD, zero D-NEW): +12 test (201→213), tutto oracle-verificato, clippy pulito.
- 12-1
9a8b69d(refactor a parità): eval.rsslots→globals: Vec<Zval>+locals: Option<Vec<Zval>>,names→global_names+local_names; macroframe_mut!(macro, non metodo, così il borrow tocca solo locals/globals ediagsresta prendibile in parallelo), accessorframe()/names();call_user_fninstalla/ripristina l'overlay locale. lower.rs: estrattostruct Scope { slots, index }, Lowerer conglobals: Scope+locals: Option<Scope>,slot_forsullo scope attivo,lower_functioninstalla overlay fresco. I 201 test restano verdi. - 12-2
a20f832(global $x;):StmtKind::Global(Vec<GlobalBinding{local,global}>). Lowering registra slot locale (alias) + slot globale pre-registrato. Eval:make_cell(&mut globals[g])+frame_mut!(self)[l] = Zval::Ref(clone)— riusa interamente loZval::Refdi 11d; global indefinito promosso a cella NULL → la scrittura crea il global. No-op a scope globale (locals.is_none()).global $$x→ Unsupported. +5 test (9, 42, 7, 3, 3_99). - 12-3
da509fb($GLOBALS['literal']):Place.slot→Place.base: PlaceBase{Local|Global}; nuovoExprKind::GlobalVar(Slot)per le letture. Lowering riconosce$GLOBALS['stringa-literal'](globals_key), pre-registra lo slot globale →$GLOBALS['n']=5crea il bare global. Fast-path assegnazione bare-var gated su baseLocal. Eval: macroslot_mut!+base_cloneinstradano i 6 place-helper (write_place/read_place_value/silent_get/unset_place/ref_source_cell/bind_ref_target) al frame globale per baseGlobal. Lettura di$GLOBALS['undef']→ warning distinto "Undefined global variable $name";isset($GLOBALS['z'])falso silenzioso. +7 test (8, 10, 5, 5, 9, nY, 7). - Scope-out confermati (D-12.6):
$GLOBALS[$dynamic],$GLOBALScome array intero (count/foreach/passaggio), globali engine — richiedono overflowHashMapruntime. Bonus emerso:$x = &$GLOBALS['y']funziona gratis (ref_source_cell base-aware).
Step 17 — espansione builtin per frequenza (gruppi string/math/array) (design pass, sessione 2026-06-14)
Stesso pattern di step 10: funzioni pure, ABI esistente
fn(&[Zval], &mut Ctx), zero modifiche all'evaluator, TDD isolato per gruppo, ognuna verificata byte-per-byte contro l'oracle PHP 8.5.7 (php -n -r). 24 builtin in 5 gruppi, scelti per frequenza d'uso nel corpus/tmp/php-src/tests+Zend/tests(vedi priorità inphp-rust-next-step7). Niente by-ref (tutte by-value). Semantiche chiave verificate sull'oracle (recon di sessione): vedi note per gruppo.
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-17.1 | Gruppo case (strtoupper/strtolower/ucfirst/lcfirst/ucwords) |
Trasformazione ASCII-only byte-wise (A-Z/a-z); i byte ≥ 0x80 restano intatti (no locale, no Unicode). ucfirst/lcfirst toccano solo il primo byte; ucwords capitalizza dopo ogni separatore (default " \t\r\n\f\v"). Stringa vuota → "". |
Oracle: strtoupper("héllo")→"HéLLO" (é intatto). PHP CLI usa la C locale → ASCII-only. |
| D-17.2 | Gruppo build (str_repeat/str_pad/chr/ord) |
str_repeat($s,$n): $n<0 → ValueError ("must be greater than or equal to 0"), $n==0→"". str_pad($s,$len,$pad=" ",$type=STR_PAD_RIGHT=1): $len<=strlen→$s invariato; pad vuoto→ValueError ("must not be empty"); type 0=LEFT,1=RIGHT,2=BOTH (both: extra a destra). chr($n): (($n % 256)+256)%256 → 1 byte. ord($s): primo byte (0 se vuota). |
Oracle-verificato. Scope-out: le Deprecation 8.5 di chr (fuori [0,255]) e ord (stringa vuota/multi-byte) — emesse dall'oracle ma rare; il valore di ritorno è comunque corretto. |
| D-17.3 | Gruppo trim (trim/ltrim/rtrim) |
Charlist default " \t\n\r\0\x0B". Charlist esplicita supporta i range a..z (come PHP: c1..c2 espande l'intervallo di byte). Trim toglie i byte in set da inizio (l)/fine (r)/entrambi. |
Oracle: trim("a1b2c","a..c")→"1b2". Il range è una feature reale di PHP, non un letterale. |
| D-17.4 | Gruppo math (intdiv/pow/sqrt/floor/ceil/round) |
intdiv: troncata verso zero; /0→DivisionByZeroError; intdiv(PHP_INT_MIN,-1)→ArithmeticError. pow: int se base+exp interi e exp≥0 (con overflow→float), altrimenti float. sqrt→float (neg→NAN). floor/ceil/round→sempre float; round($v,$prec=0) half-away-from-zero, precision negativa ok (round(1234.5,-2)→1200.0). |
Oracle-verificato: pow(2,3)→int(8), pow(2,-1)→float(0.5), round(2.5)→3.0. |
| D-17.5 | Gruppo array (range/array_slice/array_reverse/array_unique/array_sum) |
range($a,$b,$step=1): int/float/char auto-detect; float se uno qualunque di a/b/step è float; direzione da a vs b; step==0→ValueError ("cannot be 0"); range crescente con step<0→ValueError ("...for increasing ranges"); decrescente usa ` |
step |
| D-17.6 | Errori | Riusa PhpError::{ValueError,DivisionByZeroError?,TypeError,ArgumentCountError}. intdiv richiede DivisionByZeroError/ArithmeticError: se non esistono in php-types, aggiunta additiva (come step 10 fece per ValueError/ArgumentCountError). |
Messaggi byte-esatti dall'oracle. |
Sotto-suddivisione TDD step 17: 17-1 case (5 fn) · 17-2 build (4 fn) · 17-3 trim (3 fn) · 17-4 math (6 fn) · 17-5 array (5 fn). Un commit TDD-isolato per gruppo, ognuno RED→GREEN, oracle-verificato. Test in crates/php-builtins/tests/builtins.rs (registry completa → var_dump/echo).
Scope-out step 17: Deprecation 8.5 di chr/ord (D-17.2); str_word_count/wordwrap/number_format/array_map/array_filter (quest'ultime due → closures, prossima feature); range con argomenti misti char↔numerici; mb_* (multibyte).
Step 17 IMPLEMENTATO (sessione 2026-06-14, TDD, zero D-NEW): +20 test (244→264), oracle-verificato, clippy pulito. 24 builtin in 5 commit TDD-isolati, ABI fn(&[Zval],&mut Ctx) invariata, zero modifiche all'evaluator.
- 17-1 case (
crates/php-builtins/src/string.rs):strtoupper/strtolower/ucfirst/lcfirst/ucwords. ASCII-only (make_ascii_*), byte ≥0x80 intatti; helperstr_arg. +2 test. - 17-2 build (string.rs):
str_repeat(neg→ValueError),str_pad(left/right/both viapad.iter().cycle(), pad vuoto→ValueError, len≤strlen→invariato),chr(rem_euclid(256)),ord(primo byte, 0 se vuota). +5 test. NB le Deprecation 8.5 di chr/ord sono scope-out (valore corretto comunque). - 17-3 trim (string.rs):
trim/ltrim/rtrimcon driverdo_trim(left,right)+trim_mask(256-bool) che espande i rangec1..c2(php_charmask). Default" \t\n\r\0\x0B". +2 test. - 17-4 math (
crates/php-builtins/src/math.rs):intdiv(trunc verso zero;/0→DivisionByZeroError;i64::MIN/-1→ArithmeticError),pow(int^int≥0 conchecked_mul→overflow promuove a float; altrimentipowf),sqrt/floor/ceil(sempre float),round(half-away-from-zero via(x±0.5).floor/ceil, precision anche negativa). Helperas_double/double_arg/to_int_arg. +5 test. - 17-5 array (
crates/php-builtins/src/array.rs):range(int/float/char auto-detect; float se un operando è float;emit_int_rangeper int/char, count-based per float anti-drift;step==0/neg-su-crescente→ValueError),array_slice(offset/len negativi,preserve_keys),array_reverse(preserve_keys),array_unique(SORT_STRING viato_zstr, prima occorrenza, chiavi preservate),array_sum(accumuloops::add,[]→int(0)). Helperrange_num/byte0/push_entry. +6 test. - Registry (
lib.rs): +24add(...). Costanti named non lowered (es.STR_PAD_LEFT,PHP_INT_MIN): i test usano i valori literali (0/1/2) o le costruiscono (-9223372036854775807 - 1). Possibile step futuro:ConstFetchper costanti engine.
Complemento di step 14: chiude lo scope-out strict_types. Semantiche verificate sull'oracle PHP 8.5.7. In strict mode la coercizione scalare è disattivata: il tipo deve combaciare esatto, con l'unica eccezione
int→float(widening). Risultati:int←intok,int←"5"→ TypeError (int, string given),float←int→ ok (widen, niente errore),int←5.0→ TypeError (int, float given),float←floatok,string←int→ TypeError,bool←int→ TypeError,?int←nullok, return:int ← "5"→ TypeError. Messaggi TypeError identici al weak.declare(strict_types=0)→ weak (default). Nota: oggideclare(...)non è gestito affatto (→ Unsupported), quindi questo step sblocca anche il parsing dideclare.
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-16.1 | Parsing declare |
Nuovo arm Statement::Declare: estrae l'item strict_types (valore intero literal) → Program.strict: bool (1→strict; 0/assente→weak). Il body (DeclareBody::Statement, per strict_types è un ;→Nop) viene lowered; altri item (ticks/encoding) → no-op runtime. declare(...){…} colon-form → Unsupported (raro). |
Prima declare cadeva su _ => Unsupported; ora gestito. Lo strict è un flag di programma (Tier 1 = single file). |
| D-16.2 | Runtime flag | Evaluator.strict da Program.strict. coerce_to_hint(value, hint, diags, strict) ramifica sul flag. |
Riusa interamente la pipeline di coercizione di step 14 (param/default/return). |
| D-16.3 | Coercizione strict | coerce_strict(value, hint): tipo esatto richiesto; UNICA conversione implicita int→float (Long→Double). null solo se nullable. Niente coercizione né deprecation. Mismatch → None → stesso arg_type_error/return_type_error del weak. |
Verificato sull'oracle: widening int→float è l'unica eccezione in strict. |
| D-16.4 | Scope-out | strict per-call-site cross-file (rilevante solo multi-file; PHP usa il declare del file chiamante), semantica reale di ticks/encoding, declare colon-form. |
Tier 1 è single-file; gli altri declare non hanno effetto osservabile qui. |
Sotto-suddivisione TDD step 16: un solo sotto-step: parsing declare + flag strict + coerce_strict. Test: strict int←int ok, int←"5" fail, float←int widen ok, int←5.0 fail, string←int fail, ?int←null ok, return strict fail, + weak ancora coerce (regressione).
Step 16 IMPLEMENTATO (sessione 2026-06-14, TDD, zero D-NEW) — commit 43ee473: +8 test (236→244), oracle-verificato, clippy pulito. Nuovo arm Statement::Declare (estrae strict_types → Program.strict; fixa anche il fatto che declare prima era Unsupported); Evaluator.strict. coerce_to_hint guadagna il parametro strict; coerce_strict richiede tipo esatto con unica eccezione int→float widening (niente coercizione/deprecation). Mismatch → stesso TypeError del weak. Applicato a param/default/return via la pipeline di step 14. strict_types=0 → weak. Chiude lo scope-out strict_types di step 14. Scope-out residuo (D-16.4): strict per-call-site cross-file, declare colon-form, ticks/encoding reali.
L'utente ha scelto static vars dopo type-hint. Semantiche verificate sull'oracle PHP 8.5.7:
function f(){ static $n=0; $n++; echo $n; } f();f();f();→123(init una volta, persiste cross-call);static $a;(no init) →NULLpoi persiste; ricorsionefunction f($d){ static $n=0; $n++; if($d>0) f($d-1); return $n; } f(3)→4(cella condivisa tra i frame ricorsivi); isolamento per-funzione (f→1,g→101,f→2 =11012); init non-costante consentita (static $x = strlen("ab")→2, valutata alla prima call);static $a, $b=5multipli.
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-15.1 | Modello persistenza | Store statics: Vec<Option<Rc<RefCell<Zval>>>> nell'Evaluator, indicizzato da uno static_id univoco globale assegnato a lowering; persiste per tutto il run. Ogni static $x distinto = un id. |
La cella condivisa dà in un colpo: persistenza cross-call, sharing cross-ricorsione, init-una-volta. Riusa Zval::Ref di 11d (come global/$GLOBALS). |
| D-15.2 | HIR | StmtKind::StaticVar(Vec<StaticBinding>), StaticBinding { slot: Slot, id: usize, init: Option<Expr> }. Program.static_count: usize per dimensionare il Vec. |
id flat-index → store O(1) senza HashMap. |
| D-15.3 | Lowering | Statement::Static → per ogni StaticItem: slot locale (slot_for), id = self.static_count (poi += 1), init = Some(lower_expr(value)) per Concrete / None per Abstract. Lowerer field static_count; copiato in Program.static_count a fine lowering. |
id univoco e stabile tra tutte le funzioni. |
| D-15.4 | Eval | Per ogni binding: se statics[id] è None → valuta init (o Null) nel frame corrente, crea Rc::new(RefCell), salva; poi frame_mut!()[slot] = Zval::Ref(Rc::clone(cell)). |
Init solo alla prima esecuzione; re-alias su ogni call alla stessa cella persistente. |
| D-15.5 | Init non-costante | init è un Expr generico, valutato alla prima esecuzione del static (PHP 8.3+). |
Oracle: strlen("ab") → 2. |
| D-15.6 | Scope-out | static:: (late static binding, OOP), static closures (static function/fn), proprietà statiche di classe. |
Richiedono OOP. |
Sotto-suddivisione TDD step 15: un solo sotto-step (contenuto): StmtKind::StaticVar + lowering + store persistente + eval. Test: accumulate (123), ricorsione condivisa (4), isolamento per-funzione, no-init→null+persist, init non-costante, item multipli.
Step 15 IMPLEMENTATO (sessione 2026-06-14, TDD, zero D-NEW) — commit 4a48dc7: +6 test (230→236), oracle-verificato, clippy pulito. StmtKind::StaticVar(Vec<StaticBinding{slot,id,init}>) + Program.static_count. Lowering (Statement::Static → per StaticItem::{Abstract,Concrete}): slot locale + id univoco (static_count++) + init. Evaluator: statics: Vec<Option<Rc<RefCell<Zval>>>> dimensionato a static_count, persiste per tutto il run. Eval: prima esecuzione → valuta init (o Null), crea cella; ogni esecuzione → re-alias frame_mut!()[slot] = Zval::Ref(clone(cell)). Persistenza cross-call + sharing cross-ricorsione + init-una-volta in un solo meccanismo (riusa Zval::Ref di 11d). Scope-out (D-15.6): static::/static closures/proprietà statiche (OOP).
Chiude D-NEW-6 (step 8: hint accettati ma non enforced). L'utente ha scelto type-hint enforcement come prossimo step. Semantiche verificate sull'oracle PHP 8.5.7 (weak mode, default). Coercion param è più stretta del cast
(int):f(int $x); f("12abc")→ TypeError (non12); solo stringhe numeriche ben formate coercono. Risultati chiave:int<-"123"=123,int<-3.0=3 (no dep),int<-3.7=Deprecated "Implicit conversion from float 3.7…"+3,int<-"1.5"=Deprecated "…from float-string "1.5"…"+1,int<-"1.0"=1 (no dep),int<-true=1,int<-null=TypeError,int<-[1]=TypeError;float<-"1e3"=1000.0,float<-"abc"=TypeError;string<-42="42",string<-true="1";bool<-0=false,bool<-"x"=true;?int<-null=NULL; return:intcoerce uguale ma messaggio diverso. Messaggi: arg =f(): Argument #1 ($x) must be of type int, string given, called in <file> on line <L> and defined in <file>:<DL>; nullable mostra?int; return =f(): Return value must be of type int, string returned in <file>:<DL>.
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-14.1 | Scope | Enforcement SOLO dei 4 hint scalari (int/float/string/bool) + nullable ?T, in weak mode (default). Ogni altro hint (array/iterable/object/callable/nome-classe/union/intersection/mixed/void/self/…) → nessuna enforcement (accettato as-is = comportamento attuale). |
I fail D-NEW-6 sono quasi tutti coercizione scalare. Union/classi/strict richiedono molto più lavoro (e OOP). |
| D-14.2 | Rappresentazione | enum ScalarType { Int, Float, String, Bool }, struct TypeHint { kind: ScalarType, nullable: bool }. Param.hint: Option<TypeHint>, FnDecl.ret_hint: Option<TypeHint>. Lowering mappa Hint::Integer/Float/String/Bool e Hint::Nullable(inner-scalare); ogni altro Hint → None. |
HIR-level; un None significa "non enforced" (uniforma scope-out e hint assenti). |
| D-14.3 | Motore coercizione (weak) | Evaluator::coerce_to_hint(value, &TypeHint) -> Result<Zval, GivenType> (Err porta il nome-tipo PHP del valore per il messaggio). Regole sotto. Riusa numstr::parse_numeric_ex(s,false) (rifiuta trailing), convert::{dval_to_lval_safe,to_double,to_zstr,to_bool}. null→ok solo se nullable; array/object→sempre Err. |
Le primitive numeriche/convert esistono già da step 10; il motore è orchestrazione. |
| D-14.4 | Param TypeError | In run_user_fn_body, dopo aver calcolato il binding by-value (NON per Arg::Ref né per i default), applica coerce_to_hint. Err → PhpError::TypeError("{fn}(): Argument #{n} (${pname}) must be of type {hint}, {given} given, called in {file} on line {callline} and defined in {file}:{defline}"). callline = self.cur_line (linea della call, già impostata quando si valuta il Call); pname = f.slots[param.slot]; defline = f.line. |
La coercizione avviene al bind, prima del corpo. |
| D-14.5 | Return TypeError | In run_user_fn_body, dopo exec_stmts, se ret_hint Some coerce il valore di ritorno (by-value). Err → "{fn}(): Return value must be of type {hint}, {given} returned in {file}:{defline}" (formato diverso: no "called in", suffisso "returned in F:DL"). |
Solo by-value; un function &f(): int con return-by-ref resta scope-out. |
| D-14.6 | Diagnostica deprecation | float→int con frazione → Deprecated "Implicit conversion from float {repr} to int loses precision" (riusa dval_to_lval_safe). float-string→int con frazione → Deprecated "Implicit conversion from float-string "{orig}" to int loses precision" (messaggio custom: "float-string" + stringa originale quotata). |
Verificato: 3.0/"1.0" NON deprecano, 3.7/"1.5" sì. |
| D-14.7 | Scope-out | declare(strict_types=1), hint union/intersection/classe/object/array/iterable/callable/mixed/void/self/parent/static, param variadici tipati (già unsupported), coercizione su param by-ref. |
Richiedono strict-mode engine, OOP, o sono rari. |
Tabella coercizione weak (target ← sorgente):
| target | Long | Double | Bool | Str (numerica ben formata) | Str (non num.) | Null | Array |
|---|---|---|---|---|---|---|---|
| int | as-is | frac==0→trunc; else Dep+trunc | 0/1 | int→val; float→(frac==0→val; else Dep-float-string+trunc) | Err | Err* | Err |
| float | →f64 | as-is | 0.0/1.0 | →f64 | Err | Err* | Err |
| string | to_zstr | to_zstr | "1"/"" | as-is | as-is | Err* | Err |
| bool | to_bool | to_bool | as-is | to_bool | to_bool | Err* | Err |
(*) null con nullable=true → resta Null (ok). Nome-tipo per "{given}": Long→int, Double→float, Str→string, Bool→bool, Null→null, Array→array.
Sotto-suddivisione TDD step 14: 14-1 rappresentazione (TypeHint/ScalarType + lowering) + motore coercizione param (successi int/float/string/bool/nullable) + Param TypeError; 14-2 deprecation float→int (float e float-string) + return type enforcement.
Step 14 IMPLEMENTATO (sessione 2026-06-14, TDD, chiude D-NEW-6): +11 test (220→230 net, -1 test obsoleto sostituito), oracle-verificato, clippy pulito.
- 14-1
8dd9331: nuovi tipi HIRScalarType{Int,Float,String,Bool}+TypeHint{kind,nullable}condisplay_name().Param.hint+FnDecl.ret_hintvialower_hint(mappaHint::Integer/Float/String/Bool+Nullablescalare; ogni altro →None). Motorecoerce_to_hint+coerce_to_{int,float,string,bool}(free fn in eval.rs) applicano la coercizione weak al bind by-value inrun_user_fn_body; più stretta del cast(int)(solo stringhe numeriche ben formate, riusanumstr::parse_numeric_ex(s,false)). Fallimento →arg_type_errorcon messaggio PHP esatto. Sostituito il test "hint accettati ma non enforced". +5 test. - 14-2
7b4e5a1: return type coercion (inrun_user_fn_bodydopoexec_stmts, skip seby_ref) +return_type_error(formato "Return value must be of type … returned in F:DL"). Deprecation float→int (riusadval_to_lval_safe) e float-string→int (messaggio custom "float-string") già cablate in 14-1, qui testate. +5 test. - Default coercion (chiude D-NEW-6 completamente): anche i default sono coercizzati (
float $n = 0→float(0)). +1 test. - Scope-out confermati (D-14.7):
declare(strict_types=1), hint union/intersection/classe/array/iterable/mixed/void, param variadici tipati, coercizione su param by-ref.
Dialogo → l'utente ha scelto return-by-ref come prossimo step (piccolo, il modello
Zval::Refè pronto da 11d/12). Semantiche verificate sull'oracle PHP 8.5.7:function &f(){ global $x; return $x; } $y=&f(); $y=99;→ global a99;$y=f()/echo f()(contesto valore) → copia (1/5);return <non-lvalue>oreturn;in fn by-ref → Notice "Only variable references should be returned by reference" + valore (NULL per bare return);$y=&normalfn()(fn NON by-ref) → Notice "Only variables should be assigned by reference" + valore;$y=&byref_fn_che_ritorna_nonplace()→ solo il Notice interno (no outer).
| ID | Tema | Decisione | Razionale |
|---|---|---|---|
| D-13.1 | Accettare la sintassi | FnDecl.by_ref: bool (lowering legge func.ampersand); rimosso il reject "function returning by reference" in lower.rs. |
Prerequisito; il flag serve anche al call-site per decidere il Notice outer. |
| D-13.2 | Return di un place | Nuovo StmtKind::ReturnRef(Place). Eval: ref_source_cell(place.base, steps) → Flow::Return(Zval::Ref(cell)). |
Riusa interamente la macchina cell di 11d/12 (make_cell/place_cell): un return-by-ref è la promozione del place a cella condivisa. |
| D-13.3 | Quando abbassare a ReturnRef | Lowerer flag fn_by_ref (settato in lower_function). return <expr> → se fn_by_ref && <expr> è lvalue (Variable::Direct / ArrayAccess / Parenthesized) → ReturnRef(lower_place); altrimenti Return(lower_expr). |
La detection lvalue va fatta a lowering (ha lower_place); il runtime riceve già la forma giusta. |
| D-13.4 | Notice "Only variable references…" | Runtime field fn_returns_ref: bool (save/restore in call_user_fn come locals). Un StmtKind::Return (NON ReturnRef) eseguito con fn_returns_ref==true emette il Notice. |
Copre in un colpo return; e return <nonplace> dentro una fn by-ref (entrambi non sono ReturnRef). |
| D-13.5 | Call-site $y = &f() |
Nuovo ExprKind::AssignRefCall { target: Place, call: Box<Expr> }. Lowering: nel ramo &-rhs esistente, se u.operand è una Call → AssignRefCall invece di AssignRef. Eval: chiama raw (no deref); Zval::Ref(cell) → bind target alla cella; valore → se il callee NON è by-ref emette "Only variables should be assigned by reference", poi bind a cella fresca col valore. |
Un call non è un Place: variante dedicata, lascia intatto l'AssignRef di 11d. Il flag by_ref del callee (via fn_index) decide l'outer Notice (oracle F: solo inner se callee by-ref). |
| D-13.6 | Contesto valore | eval(ExprKind::Call) deref-a il risultato della user-fn (Zval::Ref → copia). I builtin non ritornano mai Ref. |
$y=f()/echo f() devono copiare; solo $y=&f() (AssignRefCall) prende la cella raw. |
| D-13.7 | Scope-out | static $x (feature separata, serve per i contatori), return-by-ref di proprietà (no OOP), return-by-ref dentro foreach. |
Fuori Tier 1 corrente; i due casi-test (global, elemento via param by-ref) non li richiedono. |
Sotto-suddivisione TDD step 13: 13-1 core return-by-ref (FnDecl.by_ref + ReturnRef + AssignRefCall + deref contesto-valore) — TDD da $x=1; function &f(){global $x; return $x;} $y=&f(); $y=99; echo $x; → 99, più elemento-via-param-byref (99) e contesto valore (echo f()→5, $y=f()→copia); 13-2 diagnostica (i due Notice via canale diags).
Step 13 IMPLEMENTATO (sessione 2026-06-14, TDD, zero D-NEW): +7 test (213→220), oracle-verificato, clippy pulito.
- 13-1
b6c76ee(core):FnDecl.by_ref(lowering leggefunc.ampersand, rimosso il reject). Dentro una fn by-ref,return <lvalue>→ nuovoStmtKind::ReturnRef(Place)che promuove il place a cella condivisa (riusa 11d/12) e ritornaZval::Ref(cell). Call-site$y=&f()→ nuovoExprKind::AssignRefCall{target,call}:assign_ref_callchiama raw (eval_call_for_ref, no deref) e aliasa la cella; risultato non-Ref → cella fresca. Contesto valore ($y=f(),echo f()):eval(Call)deref-a il risultato della user-fn. Lowering: predicatois_returnable_lvalue+ flagfn_by_refnel Lowerer. +4 test (99, 99, 5, 1). - 13-2
87f676d(diagnostica): runtime fieldfn_returns_ref(save/restore incall_user_fndaFnDecl.by_ref). UnStmtKind::Return(non ReturnRef) dentro fn by-ref → Notice "Only variable references should be returned by reference" (coprereturn;ereturn <nonplace>).assign_ref_call→ Notice "Only variables should be assigned by reference" quando il callee NON è by-ref (callee by-ref che ritorna non-place ha già emesso il suo Notice — oracle F). +3 test (canalediags). - Scope-out confermati (D-13.7):
static $x, return-by-ref di proprietà (no OOP), return-by-ref inforeach. Bonus: il modello regge anche$x = &$GLOBALS['y'](da step 12) senza modifiche.
| Fuori scope | Perché | Cosa richiederebbe |
|---|---|---|
Return by-reference (function &f()) |
— | |
static $x in funzione |
Stato persistente cross-call; serve per i contatori return-by-ref. | Slot persistente per (funzione, nome), inizializzato una volta. |
| GC ciclico | Con element-ref i cicli diventano possibili ($a[0]=&$a); leak accettato (D-R15/D-G6). |
Rc → servirebbe weak/cycle-collector. |
- 11a ✅ (
cb403bc) —Bindingenum + read/write-through +$b = &$a+unset(D-R1..R5, D-R8, D-R9). - 11b ✅ (
06ddf17) — parametri by-reff(&$x)(D-R6). - 11c ✅ (
81ae800) — builtin by-ref:array_push/sort/array_pop/array_shift(D-R7). - 11d ⏳ (design sopra) — element-ref + foreach-by-ref via
Zval::Ref(D-R10..R15), 4 sotto-step TDD.
superpowers:brainstorming breve per validare D-R1 vs alternative (è una scelta
architetturale), poi superpowers:test-driven-development partendo da 11a. Verificare
ogni semantica contro l'oracle /tmp/php-src/sapi/cli/php come fatto allo step 10.
- Buffer overflow (bounds check), 2. use-after-free (ownership), 3. leak su error path (RAII/Drop), 4. corruzioni della HashTable C (collezioni sicure), 5. race su stato globale (Send/Sync), 6. errori di refcount manuale (Rc).
OOP (classi, interfacce, traits, enum, closures, generators, fibers), exceptions
user-level (try/catch), riferimenti & dentro array + foreach-by-ref + return-by-ref
(le reference a livello di variabile sono lo step 11, vedi sezione dedicata),
include/require, namespace, eval,
superglobals web ($_GET…), resources, INI system (default hardcoded: display_errors=1,
precision=14, serialize_precision=-1), opcache/JIT, ZTS.
- D-G12 (float formatting): se il differential mostra divergenze sistematiche sulla
modalità precision=14, si porta
zend_gcvtfedelmente (~150 LOC). Accettato? - D-G8: se mago non copre un costrutto 8.5 usato dai test, fallback = skip-list, non patch a mago. Accettato?
- Ordine warning vs output bufferizzato: assumiamo stdout unbuffered interleaved (CLI default). Se i .phpt rivelano differenze, si adegua.
Prima feature di "funzioni come valori": funzioni anonime (function() use(...) {}),
arrow function (fn() => expr), chiamata dinamica ($f(), $a['k'](), IIFE), callable
stringa ($f = 'strlen'; $f(...)), e i builtin higher-order array_map / array_filter /
usort (+ is_callable, call_user_func[_array]). Sblocca lo scope-out di step 10/17.
Inclusi tutti e 4 gli extra opzionali (var_dump esatto, first-class callable strlen(...),
call_user_func_array, modi di array_filter → richiedono ConstFetch).
Semantica oracle-verificata 8.5.7 (php -n -r): use($x) cattura by value alla
definizione; use(&$x) by-ref; fn()=> auto-cattura by value (transitivo per arrow
annidate); gettype → "object"; var_dump/print_r → Closure Object con name/file/line;
dispatch $f(): Closure→invoca, stringa→user-fn poi builtin altrimenti Call to undefined function nope(), altro→Value of type int is not callable; array_map preserva le chiavi
(single) e reindicizza (multi-array); array_filter senza callback = truthy, con callback
preserva le chiavi; usort in-place by-ref, reindicizza 0..n, ritorna true; troppi pochi
argomenti a una closure → ArgumentCountError fatale.
| ID | Costrutto | Scelta Rust | Razionale |
|---|---|---|---|
| D-18.1 | Valore closure | Zval::Closure(Rc<Closure>) variante dedicata (NO oggetto OOP) |
Niente OOP ancora; anti-priming GoF (enum tipizzato > gerarchia). gettype→"object", error_type_name→"Closure". |
| D-18.2 | Storage funzioni anonime | tabella piatta Program.closures: Vec<FnDecl> + ExprKind::Closure{fn_idx, captures} |
Riusa l'intera macchina FnDecl/run_user_fn_body. Annidamento → tabella piatta, fn_idx flat. Il valore Closure porta captures: Vec<(u32 dst_slot, Zval)> (auto-contenuto: nessun parallel-array col FnDecl). |
| D-18.3 | Cattura use($a, &$b) |
by-val: deref_clone dello slot padre alla creazione (undef→Warning+Null); by-ref: condivide la cella (Zval::Ref) |
Snapshot at-definition è la semantica PHP. Lo slot padre è risolto nello scope chiamante prima di installare lo scope della closure. |
| D-18.4 | Arrow fn()=>expr |
auto-cattura by value dei free var presenti nello scope chiusura tramite AST walk ∩ slot già esistenti nello scope padre; body = return <expr> |
Distingue var del padre (catturate) da nuovi local (write interni). Euristica "lo slot esiste già nel padre" ≈ semantica PHP at-definition; var usata-ma-non-ancora-definita → Null+Warning (raro, documentato). |
| D-18.5 | Chiamata dinamica | nuovo ExprKind::CallDynamic{callee, args}; metodo call_value(&Zval, argv) dispatcha Closure / stringa / errore |
Copre $f(), $a['k'](), IIFE (function(){})(). Argomenti by value (by-ref ai dynamic call = scope-out). |
| D-18.6 | Builtin higher-order | intercettati nell'evaluator (non nella registry): array_map, array_filter, usort, is_callable, call_user_func[_array] | L'ABI builtin fn(&[Zval],&mut Ctx) non ha accesso all'evaluator per invocare la callback; infilare &mut Evaluator in Ctx litiga col borrow checker. Idiomatico: metodi dell'evaluator. usort prende arg0 by-ref (come sort). Bonus: funzionano anche con registry vuota → testabili in eval.rs. |
| D-18.7 | ConstFetch costanti named |
arm di lowering Expression::ConstantAccess → sostituzione literal da tabella costanti engine (ARRAY_FILTER_USE_KEY=2/USE_BOTH=1, STR_PAD_LEFT/RIGHT/BOTH, PHP_INT_MAX/MIN/SIZE, PHP_FLOAT_, PHP_EOL, SORT_, COUNT_*, M_PI, true/false/null) |
Sblocca i modi di array_filter e retro-sblocca l'ergonomia di TUTTI i builtin con flag (step 17). Backlog #3. Costante sconosciuta → resta Unsupported (no const utente). |
| D-18.8 | Type hint callable |
accettato, non enforced (lower→None) |
Coerente con D-14.1 (hint non scalari → nessuna coercizione). Funziona già "gratis". |
| D-18.9 | var_dump/print_r closure | formato 8.5 esatto object(Closure)#N (3){name,file,line} / Closure Object(...); contatore object-id |
Extra richiesto. name = {closure:<file>:<line>}. |
| D-18.10 | First-class callable strlen(...) |
produce una Closure che incapsula un nome (ClosureKind::Named) |
Extra richiesto (sugar 8.1). var_dump mostra object(Closure). |
- 18-1: infra
Zval::Closure+function(){} use(...)(by-val/by-ref) +$f()(CallDynamic/call_value/call_closure) + IIFE +gettype="object". ArmZval::Closurenei funnelops/convert/zval(non esaustivi). - 18-2: arrow function + free-var walk + cattura by-value (incl. annidate).
- 18-3: callable stringa +
is_callable+call_user_func+call_user_func_array+ conferma hintcallable. - 18-4:
ConstFetch+ tabella costanti engine. - 18-5:
array_map(single/multi/chiavi) +array_filter(con/senza callback + modi via ConstFetch) +usort. - 18-6: first-class callable
strlen(...). - 18-7: var_dump/print_r esatto per closure + docs/metrics.
Closure::bind/bindTo/call/fromCallable e static closures (richiedono $this/OOP);
argomenti by-ref ai dynamic call ($f(&$x)); string-call di un builtin by-ref ($f='sort'; $f($a));
spread ...$args negli argomenti; callable array [$obj,'m']/['Cls','m'] (OOP);
cattura by-value di var del padre usata-ma-non-ancora-definita testualmente (→ Null+Warning).
Tutti e 7 i gruppi TDD shippati come da design (design d9c6fed; 18-1 9a556ff,
18-2 a899bd1, 18-3 f8a7a26, 18-4 c30263e, 18-5 15c2197, 18-6 cef7e5f,
18-7 732e6b7). Nessuna D-decisione riaperta. Note di implementazione in
diary/metrics.md § "Step 18". Due divergenze note documentate nello scope-out di
18-7 (object-id non riciclati; first-class callable di builtin senza parameter[]).
D-18.8 confermata "gratis": il hint callable lowering→None passa il valore senza
enforcement. Object→string di una closure: PHP fa un fatal Error, il funnel
infallibile to_zstr emette invece un Warning + placeholder (edge non testato,
rivedere con OOP). Terza divergenza var_dump (corpus): PHP aggiunge ["static"]
con le variabili catturate per le closure con use/arrow — omessa (richiede
recursion-guard per use(&$f)); dettaglio in diary/metrics.md § Step 18.
Il blocco più grande di unsupported nel corpus (~5028 casi). Scope Full Tier-1
deciso col Decider (2026-06-14): classi, proprietà (default + visibility), metodi,
__construct, $this, new, semantica handle, read/write proprietà,
ereditarietà (extends/parent::/self::), membri static, costanti di
classe, instanceof/interfaces, abstract/final, __toString,
Closure::bind/bindTo/fromCallable + static closures, var_dump/print_r esatto
con recursion-guard (retro-sblocca anche ["static"] delle closure dello step 18).
Eccezioni (try/catch/finally/throw + Exception/Error) = step 20 separato
(control-flow a sé, riusa le classi di qui).
Semantica oracle-verificata 8.5.7 (php -n -r): assegnare un oggetto copia
l'handle (mutazioni condivise, contrasta gli array COW); var_dump →
object(C)#N (k) { ["p"]=>…, ["p":protected]=>…, ["p":"C":private]=>… }; gettype
→ "object"; $p instanceof C → bool; proprietà non dichiarate sono dinamiche
(deprecation 8.2, ma supportate); new C senza () legale; $this fuori da metodo
→ Error; accesso a proprietà private/protected dall'esterno → Error.
| ID | Costrutto | Scelta Rust | Razionale |
|---|---|---|---|
| D-19.1 | Valore oggetto | Zval::Object(Rc<RefCell<Object>>) |
Semantica handle: clone condivide l'Rc, mutazione via RefCell visibile a tutti. NON Rc::make_mut (≠ array COW). gettype→"object", error_type_name→nome classe. |
| D-19.2 | Struct oggetto | Object { class: ClassId, props: Props, id: u32 } dove Props è una mappa ordinata Box<[u8]>→Zval (riusa il pattern PhpArray: Vec di entry + index, ordine di inserzione per var_dump) |
Le proprietà PHP conservano l'ordine di dichiarazione/assegnazione; var_dump lo riflette. Oggetti = poche prop → struttura leggera. |
| D-19.3 | Class table | Program.classes: Vec<ClassDecl> hoisted al lowering (come functions/closures) + name→ClassId a runtime (case-insensitive) |
Le classi sono visibili prima della decl (hoisting PHP, salvo extends di classe condizionale → scope-out). ClassId = usize. |
| D-19.4 | ClassDecl | { name, parent: Option<ClassId>, interfaces: Vec<ClassId>, props: Vec<PropDecl>, static_props: Vec<…>, methods: Vec<MethodDecl>, consts: Vec<(name,Expr)>, is_abstract, is_interface } |
Risoluzione extends/implements per nome→id al lowering (forward-ref ok: 2-pass). |
| D-19.5 | Metodo | MethodDecl { fdecl: FnDecl, name, vis: Visibility, is_static, is_abstract, is_final, defining_class: ClassId } con slot riservato per $this nel frame del metodo |
Riusa interamente FnDecl/run_user_fn_body. $this è una var normale: il lowerer pre-registra lo slot this nello scope del metodo e lo memorizza; il dispatch lo lega all'handle. |
| D-19.6 | new C(args) |
nuovo ExprKind::New { class: ClassRef, args: Vec<Expr> }; crea Object con prop default valutate per-istanza, poi chiama __construct se esiste |
Default = Expr valutati al new (literali / self::CONST). ClassRef = nome literal (Tier-1) o new $var/self/static (D-19.16). |
| D-19.7 | Method call | nuovo ExprKind::MethodCall { object, method, args } (e NullSafe); risolve il metodo risalendo la catena parent, installa frame, lega $this, esegue |
$obj->m(). Dispatch = call_method(obj, class_start, name, argv). Metodo assente → __call (scope-out) o Error. |
| D-19.8 | Property read | ExprKind::PropGet { object, name } (+ NullSafe); legge dalla mappa prop dell'oggetto (no risalita: le prop ereditate sono già materializzate nell'istanza) |
$obj->p, $this->p. Prop assente → Warning "Undefined property" + Null. Nome dinamico $obj->$n → scope-out parziale (literal-first). |
| D-19.9 | Property write | estendere PlaceStep con Prop(Box<[u8]>); place_cell/write_into/navigazione entrano nel RefCell dell'oggetto (condiviso, niente write-back COW) |
$obj->p = v, $this->p = v, compound/++/??=, $obj->arr[] = v, nested $a->b->c. Punto più delicato → gruppo 19-2 isolato. Prop inesistente in write → creata (dinamica). |
| D-19.10 | Ereditarietà | extends unico (PHP single-inheritance); prop ereditate copiate nella decl figlia al lowering (flatten), metodi risolti a runtime risalendo parent |
Flatten prop = istanza self-contenuta; metodi via catena per supportare override + parent::. |
| D-19.11 | parent:: / self:: |
self = classe definente il metodo corrente; parent = il suo parent; risolti via contesto runtime (cur_class/cur_static_class) |
parent::__construct(), self::method(), self::CONST. |
| D-19.12 | static:: (LSB) |
late static binding minimale: cur_static_class = classe dell'oggetto/chiamata reale, propagata nelle call |
new static(), static::method(). |
| D-19.13 | Visibility | enum Visibility {Public, Protected, Private}; enforcement all'accesso esterno (Error PHP-esatto); usata da var_dump (:protected, :"C":private) |
Default public. Accesso da metodo della stessa classe (o discendente per protected) consentito. |
| D-19.14 | Static members | static_props: Vec<(name, vis, cell: Rc<RefCell<Zval>>)> per-classe nel runtime (persistono per il run, init una volta); Class::$p, static::$p, self::$p |
Riusa il pattern statics dello step 15 (cella persistente). |
| D-19.15 | Class constants | Class::CONST, self::CONST, parent::CONST; tabella consts per-classe, valutate lazy/al primo accesso, risalita per ereditarietà |
Default di prop possono riferirle (D-19.6). |
| D-19.16 | instanceof |
operatore: $x instanceof C true se la classe di $x è C, un suo antenato, o un'interfaccia implementata (transitiva) |
Mago: instanceof come binary/op dedicato → nuovo ExprKind o BinOp. |
| D-19.17 | interfaces / abstract / final | interface = ClassDecl con is_interface (solo costanti + metodi astratti); implements riempie interfaces; abstract class/abstract function non istanziabili/da implementare; final non overridabile/estendibile (enforcement) |
Le interfacce partecipano a instanceof. |
| D-19.18 | __toString |
object→string (echo, ., (string), sprintf %s) chiama __toString se definito, altrimenti Error "Object of class C could not be converted to string" |
Sostituisce il placeholder/Warning del funnel to_zstr (debito step 18 chiuso). Richiede che to_zstr possa rientrare nell'evaluator → gestito a livello evaluator, non in convert.rs. |
| D-19.19 | Closure::bind/bindTo/fromCallable + static closures |
Closure acquisisce bound_this: Option<Zval::Object> + scope: Option<ClassId>; $this dentro la closure legato; static function(){} = nessun bind |
Chiude lo scope-out dello step 18. fromCallable = wrap di callable in Closure. |
| D-19.20 | var_dump/print_r + recursion-guard | formato 8.5 esatto con annotazioni visibility + guardia di ricorsione generale (*RECURSION*) su oggetti/array già in corso di dump |
Retro-sblocca ["static"] delle closure catturanti (step 18). Set di puntatori "in-progress" durante il dump. |
- 19-1 Infra:
Zval::Object(Rc<RefCell<Object>>)+Object/Props+Program.classes/ClassDecl/MethodDecl+ loweringclass(prop+metodi, hoisted, 2-pass) +new C(args)(ExprKind::New) +__construct+$this+$obj->m()(ExprKind::MethodCall) + prop read (ExprKind::PropGet) +gettype/error_type_name. ArmZval::Objectnon-esaustivi inops/convert/zval/var_dump. - 19-2 Write-path proprietà:
PlaceStep::Prop+$obj->p = v/$this->p = v+ compound/++/??=+$obj->arr[] = v+ nested$a->b->c+isset/empty/unsetsu proprietà. - 19-3 Ereditarietà:
extends, risoluzione metodi su catena,parent::m(), prop ereditate (flatten),self::, enforcement visibility public/protected/private. - 19-4 Static + costanti:
static $prop/Class::$p/static::$p/self::$p,static::LSB,Class::m()(static call),const,Class::CONST/self::CONST/parent::CONST. - 19-5
instanceof+ interfaces + abstract/final:interface/implements,instanceoftransitivo, abstract non istanziabile, final non overridabile. - 19-6 Magic
__toString(object→string nei vari contesti) +Closure::bind/bindTo/fromCallable+ static closures. - 19-7 var_dump/print_r esatto per oggetti + recursion-guard generale (+
["static"]closure) + docs/metrics + validazione corpus.
try/catch/finally/throw + gerarchia Exception/Error built-in (step 20);
generators/yield, fibers; traits (use dentro classe); enum (puro/backed);
anonymous class (new class {}); namespace + ::class; magic dinamici
__get/__set/__isset/__unset/__call/__callStatic/__invoke; readonly
enforcement; property hooks 8.4; clone/__clone; nomi membro dinamici complessi
($obj->{$expr}, $obj->$$x); Stringable/ArrayAccess/Iterator/Countable
(interfacce magiche); ::class su istanza; get_class/get_object_vars/altri
builtin di reflection (valutare a parte); covarianza/contravarianza tipi; GC ciclico
(handle + prop creano cicli → leak accettato come gli element-ref, D-R15).
Tutti e 7 i gruppi shippati come da design (design 43379f1; 19-1 92b8360,
19-2 ba0c2e4, 19-3 1b628cd, 19-4 bae3c7a, 19-5 1e07a13, 19-6 5d59ba3,
19-7 = questo commit docs). Nessuna D-decisione riaperta.
- 19-1:
Zval::Object(Rc<RefCell<Object>>)(handle semantics) +Object/Props(php-types) +Program.classes/ClassDecl/MethodDecl+ lowering classe (2-pass hoist) +new+__construct+$this=ExprKind::This+$obj->m()+ prop read- write semplice (
PlaceStep::Prop).
- write semplice (
- 19-2: write-path completo proprietà (compound,
++=IncDecPlaceche copre anche$a[k]++,??=,$o->arr[], nested, isset/empty/unset). - 19-3: ereditarietà (
extends, risoluzione metodi su catena, override, prop flatten parent-first),parent::/self::(=ExprKind::StaticCall+ClassRef,cur_class), enforcement visibility public/protected/private (read+write+metodi). - 19-4: costanti di classe (
Class::C/self::C/::class), static props (cella persistente per-declaring-class,Class::$pread/write/compound/incdec), static method call, late static binding (cur_static_class,new static,static::m(), forwarding self/parent/static). - 19-5:
instanceof(=ExprKind::InstanceOf, transitivo su catena+interfacce),interface/implements, abstract non istanziabile (fatal runtime), metodi abstract = solo firma (skip al lowering). - 19-6:
__toString(helperstringifyin echo/concat/(string); chiude il debito step-18 dito_zstr), closure bind$this(Closure.bound_this, cattura alla creazione per closure/arrow non-static;static fnno-bind),bindTo/call/Closure::bind/fromCallable. - 19-7: var_dump/print_r esatti per oggetti con annotazioni visibility
(
["p":protected],["p":"C":private]; print_r[p:C:private]) viaObjectInfo/PropVisportati nel valore (shape per-classe cache); recursion guard generale (*RECURSION*) su oggetti e array (fixa anche un loop latente su array auto-referenziali).
Validazione corpus: /tmp/php-src/tests/classes ora 57 pass / 45 fail / 181
skip (102 runnable; prima dello step 19 erano ~tutti skip "unsupported").
Scope-out (debito esplicito → futuri step): final enforcement (extend/override
= fatal compile-time con formato diverso da "Uncaught"); closure instanceof Closure (le closure non sono object table-backed); scope binding delle closure per
accesso a private (solo $this è legato); sprintf %s __toString (il builtin non
può rientrare nell'evaluator); closure ["static"] in var_dump (il recursion-guard
ora c'è, ma le var catturate non sono ancora esposte); __get/__set/__call e gli
altri magic dinamici; traits; enum; anonymous class; nomi membro dinamici
($o->$n); dynamic-prop deprecation 8.2; covarianza tipi. Eccezioni (try/catch/
throw + Exception/Error) = step 20.
Obiettivo: control-flow di unwinding che riusa interamente le classi dello step
19. throw <expr>, try { } catch (T $e) { } finally { }, gerarchia built-in
Throwable/Exception/Error + sottoclassi SPL, accessor (getMessage/getCode/
getPrevious/getLine/getFile/getTrace/getTraceAsString), rendering "Uncaught".
getCode()defaultint(0);getMessage()defaultstring(0) "".__construct(message="", code=0, previous=null);getPrevious()ritorna la prev.- Ordine
finally:try→catch→finally→codice dopo (es.tcfafter). finallyconreturnsovrascrive ilreturn/throwdel try (finvince).finallygira anche se il try ritorna (return "t"in fn → stampafpoit).- Gerarchia:
ErrorNON è preso dacatch (Exception);Throwableprende tutto.RuntimeException/InvalidArgumentException→instanceof Exception+Throwable;InvalidArgumentException instanceof LogicException= true. - Multi-catch
catch (A | B $e); catch senza variabilecatch (Exception). throwcome espressione ($x ?: throw new …).- Eccezione non catturata (exit 255):
\nFatal error: Uncaught <Class>: <msg> in <file>:<line>\nStack trace:\n#0 {main}\n thrown in <file> on line <line>\nLa<line>è la linea di creazione (new), recuperabile viagetLine(). - Errori engine catturabili:
TypeError/DivisionByZeroError/… sono Throwable. var_dumpdi un'eccezione espone 7 prop interne (message/string/code/file/line/ trace/previous) con annotazioni:protectede:"Exception":private→ fuori scope step 20 (richiede le pseudo-prop internestring/trace; debito).
- Classi built-in via PRELUDE PHP. Un sorgente PHP statico (
PRELUDE_SRCinlower.rs) definisceinterface Throwable,Exception/Error(propsmessage/code/file/lineprotected,previousprivate;__construct+ accessor con corpi HIR reali) e le sottoclassi SPL (LogicException/RuntimeException+ figlie,TypeError/ValueError/ArithmeticError/DivisionByZeroError/ArgumentCountError/ErrorException/UnhandledMatchError). Lowerato con unLowererusa-e-getta; leClassDeclowned risultanti sono iniettate in testa aProgram.classes(ids 0..N) PRIMA dell'hoisting delle classi utente. Cosìextends Exception,instanceof,resolve_class_ref,collect_props(init prop),class_shape(var_dump) eparent::__constructfunzionano GRATIS con la macchina dello step 19 — zero dispatch speciale per i metodi. - Unwinding via
PhpError::Thrown(Zval). Nuova variante che trasporta l'oggetto lanciato; si propaga da sola attraverso ogni?ineval(espressioni) edexec_stmt(statement) — copre throw in profondità (throw-expr, throw dentro metodo/funzione).PhpErrorperde i derivePartialEq, Eq(Zval non è Eq: ha f64); nessuno li usava.class_name()/message()restano totali (sentinel perThrown, mai usati su quel ramo: il rendering lo gestisce a parte). - HIR.
StmtKind::Try { body, catches: Vec<CatchClause>, finally: Vec<Stmt> }(finallyvuoto = assente);struct CatchClause { types: Vec<Box<[u8]>>, var: Option<Slot>, body };ExprKind::Throw(Box<Expr>). - Lowering.
Statement::Try→ estraeblock/catch_clauses/finally_clause; ogni catch:hint(Identifier oUnion→ lista nomi),variable→ slot opzionale.Expression::Throw→ExprKind::Throw. - Eval.
exec_stmtTry: esegue body; suErr(Thrown)prova i catch peris_instance_of(obj_class, type_id)(riusa step 19-5); lega$e, esegue il body del catch.finallygira SEMPRE (anche suErr/Flownon-Normal): se finally esce Normal propaga l'esito di try/catch, altrimenti il control-flow di finally vince.ExprKind::Throw→Err(PhpError::Thrown(obj)).eval_new: per gli oggetti Throwable settaline(linea delnew) efilePRIMA del costruttore. - Rendering.
render_fatalramoThrown: classe daobj.class_name, msg/line da propmessage/line. - 20-3: errori engine (
PhpError::TypeError(..)ecc.) resi catturabili — al catch-site un errore non-Thrownè confrontato perclass_namecontro la gerarchia e, se preso, sintetizzato in un oggetto della classe corrispondente. Uncaught invariato (i 377 test restano verdi).
- 20-1: prelude + throw stmt/expr + try/catch (single/multi/no-var) + accessor + uncaught rendering.
- 20-2: finally (normal/return/throw/break + finally-overrides).
- 20-3: engine errors catturabili + sottoclassi utente/
parent::__construct+ catenegetPrevious+ validazione corpusZend/tests/exceptions.
var_dump/print_r esatto delle eccezioni (pseudo-prop interne string/trace);
stack-trace reale con frame (resta #0 {main}); getTrace() = [];
set_exception_handler/set_error_handler; finally con eccezione che ne maschera
un'altra in modo annidato oltre il caso base; coercizione scalare dei param di
__construct (lasciati untyped); DesiredException::__construct con typed ?Throwable.
TDD, +26 test (377→403), clippy pulito, zero D-NEW. 3 gruppi:
- 20-1 prelude (
PRELUDE_SRCin lower.rs, lowerato da unLowererusa-e-getta, classi owned iniettate in testa aProgram.classes; fixhoist_classesper offsettare gli id suself.classes.len());PhpError::Thrown(Zval)(persi i derivePartialEq/Eq);StmtKind::Try+CatchClause,ExprKind::Throw;handle_thrown(match peris_instance_of, single/multi/no-var);eval_newsettaline/filesui Throwable;render_fatalramoThrown. accessor (getMessage/Code/Previous/ Line/File) = corpi HIR reali del prelude. - 20-2 finally: la logica era già nel Try arm di 20-1 (finally gira sempre; il suo control-flow sovrascrive l'esito di try/catch). 8 test oracle (order, return, override, rethrow, break/continue in loop, return-inghiotte-throw).
- 20-3 errori engine catturabili (
handle_thrownrisolveclass_name()→classe prelude esynthesize_throwablematerializza l'oggetto solo se un catch lo lega); sottoclassi utente +parent::__construct+ catenegetPrevious+ throw-espressione (tutto gratis da prelude+step19).
Validazione corpus Zend/tests/{exceptions,throw,try}: 36 pass / 52 fail / 91
skip (179; prima ~tutti skip:unsupported). Fail residui = scope-out: stack-trace con
frame reali (#0 foo(...); noi #0 {main}), foreach su oggetti/Generator, stdClass
non nel prelude, magic methods.
stdClassaggiunto al prelude (new stdClass+ dynamic props già supportate dallo store oggetti).get_class([$obj])/get_parent_class([$obj|$class])come builtin intercettati nell'evaluator (dispatch_class_introspection, leggono$this/class table): get_class no-arg usa$this, non-oggetto → TypeError; get_parent_class senza parent →false.- +5 test (403→408), clippy pulito. (Corpus invariato: i fail residui dipendono dagli stack-trace con frame reali, non da queste classi/builtin.)
Generato con assistenza AI (Claude Opus 4.8, 1M context). Oracle: PHP 8.5.7 (
/tmp/php-src/sapi/cli/php -n). Strategia legacy-port: full-port semantica, verificata byte-per-byte contro l'oracle.
Scoperta chiave dalla recon dell'infrastruttura step 19: il lowering non
appiattisce metodi/prop nelle classi — li tiene per-classe e cammina la catena
parent a runtime (resolve_method, instance_props, static_prop_cell). I
trait in PHP sono copy-paste semantico (i membri del trait diventano membri
del consumer come se fossero dichiarati lì). Quindi la mossa giusta è: lowerare i
trait in una tabella interna del Lowerer e copiare i loro membri dentro il
ClassDecl del consumer al momento del lowering. Tutta la macchina runtime
(dispatch metodi, $this/self/static/new static, visibility, static props
keyed (ClassId,name), costanti, var_dump con visibility) si riusa senza una
riga nuova nell'evaluator — stesso pattern del prelude PHP dello step 20.
Conseguenze semantiche che cadono GRATIS dal flatten (tutte verificate oracle):
self/static/new staticnel metodo trait → risolvono alla classe consumer, perché il MethodDecl vive inC.methodsecur_class=C al dispatch.- static prop per-consumer (
A::inc,A::inc,B::inc=1,2,1): la copia rende C la classe dichiarante, e lo store èHashMap<(decl_class,name),cell>→ celle separate per ogni consumer. - metodo del trait batte il metodo del parent: il metodo flattenato è "proprio"
del consumer, e
resolve_methodcontrolla i metodi propri prima di salire al parent. - var_dump/print_r: le prop del trait risultano prop del consumer con la loro visibility → output identico a PHP senza casi speciali.
| ID | Tema | Scelta | Razionale |
|---|---|---|---|
| D-21.1 | Storage trait | Tabella traits: HashMap<Vec<u8>, LoweredTrait> interna al Lowerer, NON in Program.classes. |
I trait non sono tipi né istanziabili: tenerli fuori dalla class table rende new T/instanceof T falliscono/falsi senza codice speciale. |
| D-21.2 | Statement::Trait |
Lowerato come una classe senza inheritance: methods/props/static_props/consts + le sue stesse clausole use (per nesting). |
Riusa lower_method/lower_class_const/prop lowering esistenti. |
| D-21.3 | Flatten in lower_class |
ClassLikeMember::TraitUse → risolvo i nomi trait, applico adattamenti, copio i membri risultanti in testa ai vec del consumer (prima dei membri dichiarati dalla classe). |
I membri propri della classe devono vincere: li aggiungo dopo, ma escludo a monte il membro-trait omonimo (vedi D-21.4) così l'ordine serve solo per var_dump (prop trait prima, come PHP). |
| D-21.4 | Precedenza classe>trait | Se la classe dichiara un metodo/prop con lo stesso nome di uno del trait, il membro del trait è scartato silenziosamente (nessun conflitto). | Semantica PHP: l'override esplicito della classe non è una collisione. |
| D-21.5 | Collisione tra trait | Due trait nello stesso use con metodo omonimo, non risolto da insteadof → Fatal: Trait method B::say has not been applied as C::say, because of collision with A::say. |
Messaggio oracle esatto. Emesso come errore di lowering→fatal rendered. |
| D-21.6 | insteadof |
A::m insteadof B, C; → tieni A::m, escludi m da B e C. |
TraitUsePrecedenceAdaptation{method_reference:A::m, trait_names:[B,C]}. |
| D-21.7 | as alias |
T::m as [vis] alias; → aggiungi un metodo extra di nome alias (clone di T::m) con visibility eventualmente modificata. T::m as vis; (senza alias) → cambia solo la visibility del metodo m flattenato. |
TraitUseAliasAdaptation{method_reference, visibility:Option<Modifier>, alias:Option<Ident>}. Verificato: f as protected blocca la chiamata esterna con Call to protected method C::f() from global scope. |
| D-21.8 | Trait usa trait | trait B { use A; } → risoluzione transitiva con memoizzazione: prima di flattenare B in C, B ha già i membri di A appiattiti dentro di sé. |
Ricorsione su traits map; cache del set risolto per evitare ri-lavoro/cicli. |
| D-21.9 | static props/consts | Copiati come StaticPropDecl/ClassConstDecl propri del consumer. |
static keyed per decl-class → per-consumer; const flat (i trait non hanno catena). |
| D-21.10 | instanceof T (trait) |
false, nessun errore. |
I trait non sono in class table; instanceof su nome ignoto è già false (da verificare/forzare). |
| D-21.11 | abstract nel trait | Metodo abstract nel trait → raccolgo i nomi richiesti; se il consumer concreto non li implementa (né classe né altro trait) → Fatal: Class C contains N abstract method(s) and must therefore be declared abstract or implement the remaining methods (C::f). |
Richiede tracciare i nomi abstract (oggi droppati a lower.rs:776). Aggiunta minima. |
Scope-out v1 (debito esplicito): conflitto di proprietà con default diversi tra
due trait (PHP: Fatal in alcuni casi, warning in altri) → terremo "ultimo vince"
o "primo vince" documentato; props con stesso nome+default identico mergiano senza
errore (caso comune). Niente abstract/final su trait const, niente
__CLASS__/__TRAIT__ magic constant nei metodi trait (constant non ancora
lowerate genericamente). Niente trait con proprietà tipizzate enforced.
- Nessun nuovo nodo runtime. Eventuale aggiunta:
ClassDecl.abstract_methods: Vec<Box<[u8]>>(o un check fatto interamente a lowering) per D-21.11.
- 21-1 Core flatten:
Statement::Trait+use T;singolo → metodi + prop istanza flattenati;$thisnel metodo trait → consumer; classe override trait (D-21.4); trait override parent (D-21.3). - 21-2 Multi-trait + static + const:
use A, B;membri disgiunti; static prop per-consumer; trait const (8.2); metodi statici +self::/static::/new static. - 21-3 Conflict resolution: collisione → Fatal (D-21.5, msg esatto);
insteadof(D-21.6);asalias + cambio visibility con/senza rename (D-21.7). - 21-4 Nested + abstract + instanceof: trait-usa-trait transitivo (D-21.8),
cross-trait
$this->other(); abstract richiesto non implementato → Fatal (D-21.11);instanceof T→ false (D-21.10). - 21-5 var_dump/print_r + corpus + docs: dump prop trait come prop consumer;
validazione
Zend/tests/traits; docs + memory.
Generato con assistenza AI (Claude Opus 4.8, 1M context). 5 gruppi TDD, +25 test (408→433), clippy pulito, ZERO modifiche all'evaluator (eccetto il rendering dei compile-fatal). Commit: design
7ee593f, 21-11548f87, 21-2ebdae79, 21-3933b19a, 21-4266e7be, 21-5 (questo).
Architettura confermata = flatten-at-lowering. Lowerer.traits: HashMap<Vec<u8>, LoweredTrait> (i trait NON entrano in Program.classes).
lower_traits (raccolta AST) → resolve_trait (memoizzato, cycle-guard, risolve
i use annidati prima del flatten) → flatten_into (copia i membri nei vec del
consumer). lower_class gestisce ClassLikeMember::TraitUse. Tutta la macchina
step-19 (resolve_method, static_prop_cell keyed (ClassId,name),
self/static/new static, visibility, var_dump con visibility) riusata intatta.
Gruppi:
- 21-1 core flatten: metodi+prop istanza;
$this/get_class→consumer; precedenza classe>trait (D-21.4) e trait>parent (D-21.3); multi-trait disgiunti. - 21-2 static+const: static prop per-consumer (1,2,1) gratis dal keying;
self::/static::/new static; trait const; abstract presente ma soddisfatto. - 21-3 conflict resolution:
insteadof(D-21.6);asalias + cambio visibility con/senza rename (D-21.7, helperfind_trait_method); collisione →LowerError::Fatalreso byte-esatto dacompile_fatal_outcome(D-21.5). - 21-4 nested transitivo (D-21.8); cross-trait
$this->other(); abstract non implementato → fatal sing/plur con listaC::f, C::g(D-21.11, helperabstract_unimplemented_fatal);instanceof T→false gratis (D-21.10, trait non in class table + instanceof su nome ignoto giàfalse). - 21-5 var_dump/print_r + corpus.
SCOPERTA ordine prop (corretta in 21-5): PHP elenca prima le prop proprie
del consumer, poi quelle del trait (e ricorsivamente: own → nested-trait), NON
in ordine testuale del use. Es. class C { use T; public $c; } con T{$a,$b}
→ dump c, a, b. Corretto invertendo la concatenazione in resolve_trait e
lower_class (own.extend(trait) invece di trait.extend(own)).
Validazione corpus Zend/tests/traits: 42 pass / 38 fail / 136 skip
(216 tot; prima ~tutti skip:unsupported "class member"). Pass-rate runnable 52.5%.
Fail categorizzati:
- Classe B (corpus stale, noi corretti vs binario):
conflict001/003,language011, varierror_*— il nostro fatal di collisione è byte-identico al binario 8.5.7 (incluso\nStack trace:\n#0 {main}\n), ma l'EXPECTF del.phptomette lo stack-trace. Verificato eseguendo l'oracle reale. - Scope-out (feature adiacenti):
static_004/trait_type_errors→__callStatic/magic methods (backlog separato);constant_004..021→ accesso diretto a costante di trait + costanti incompatibili;property003..008→ proprietà con default incompatibili tra trait (noi "first-wins", D-21 scope-out). - Type-error qualification: messaggi
C::test1()(qualificato) vs nostrotest1()per metodi trait-flattenati con type hint — minore, scope-out.
Scope-out riepilogo: magic methods (__callStatic/__get/…); accesso
diretto Trait::CONST/Trait::$static; fatal "incompatible property/constant
definition" (teniamo first-wins); abstract soddisfatto SOLO da metodo concreto
ereditato dal parent (non camminiamo la catena per il check); type-error message
non qualificato col nome classe per metodi trait.
__toString è già fatto (19-6, D-19.18). Questo step aggiunge gli accessor
overload: __get, __set, __isset, __unset, __call, __callStatic,
__invoke. Architettura: intercetto nei punti di dispatch già esistenti
dello step 19, zero modifiche all'HIR/lowerer (i magic method sono normali
metodi del ClassDecl, risolti via resolve_method).
| ID | Punto | Scelta | Razionale |
|---|---|---|---|
| D-22.1 | Dispatch points | __get→read_property; __set→write_place (solo step finale [Prop]); __isset→silent_get; __unset→unset_place; __call→call_method; __callStatic→call_static; __invoke→call_value. |
Tutti i path di assegnazione passano da write_place/read_place_value, quindi Assign/AssignOp/IncDec/??= su proprietà sono coperti intercettando i due funnel + read_property. |
| D-22.2 | Trigger property | Magic se la proprietà manca (!props.contains) oppure è inaccessibile dallo scope corrente (resolve_prop_decl + visible_from). Altrimenti accesso diretto. |
Verificato su oracle: una private letta da fuori instrada a __get/__set. |
| D-22.3 | Trigger method | __call se il metodo non risolve (resolve_method=None) oppure risolve ma non visibile dallo scope (check_method_access fallisce). Idem __callStatic. |
Oracle: chiamare un metodo private da fuori → __call; da dentro la classe → diretto. |
| D-22.4 | Recursion guard | magic_guard: HashSet<(u32 id_oggetto, MagicAccess, Vec<u8> nome)>. Mentre __get/__set/__isset/__unset gira per (obj,kind,name), un accesso annidato alla stessa prop con lo stesso kind bypassa il magic e va al path diretto. |
Replica i bit di guardia per-property di Zend; $this->$n dentro __get($n) non ricorre. Kind separati (GET/SET/ISSET/UNSET) come in Zend. |
| D-22.5 | Firme | __get($name), __set($name,$value), __isset($name)→bool, __unset($name), __call($name,$args_array), __callStatic($name,$args_array), __invoke(...$args). Il nome è Zval::Str; gli args di __call sono un PhpArray lista. |
Spec PHP. |
| D-22.6 | empty() / ??= |
empty($o->p) = !__isset o (__isset true ma __get falsy). $o->p ??= v = se !__isset o __get null → __set. |
Oracle: empty chiama __isset poi __get. |
| D-22.7 | is_callable($obj) |
true sse la classe ha __invoke (risolto su per la catena). call_value(Object) → __invoke con tutti gli argomenti; altrimenti Error "not callable". |
array_map($obj,…) usa __invoke (passa già da call_value). |
- Indirect modification of overloaded property:
$o->magicProp[] = v/$o->magicProp->x = vquandomagicPropè gestita da__get/__set(multi-step path che non è[Prop]finale). PHP emette Notice e/o lavora su una copia; noi lasciamo cadere al path diretto (no magic oltre il primo livello). Documentato, non emulato. __getby-reference (__getche ritorna&): scope-out, il nostro__getritorna per valore.__setsu offset di stringa / array append via magic: scope-out.
- 22-1
__get/__set: read/write prop mancante; prop private inaccessibile da fuori; accesso diretto quando esiste+accessibile; compound+=su magic prop (read__get→ write__set); recursion guard$this->$n. - 22-2
__isset/__unset:isset($o->p)via__isset;empty()=__isset+__get;unset($o->p)via__unset;??=via__isset; guard. - 22-3
__call/__callStatic: metodo ignoto su istanza; metodo private da fuori; static ignoto; args come array;__calldentro scope = diretto. - 22-4
__invoke:$obj(...)viaCallDynamic;call_user_func($obj);is_callable($obj)=true;array_map($obj,…).
Spedito in 5 commit (22-1 → 22-5). Infra: enum MagicAccess{Get,Set,Isset, Unset} + magic_guard: HashSet<(u32 id_oggetto, MagicAccess, Vec<u8>)>;
helper magic_prop_method (decide magic-vs-diretto: prop mancante o non
visibile + metodo presente + non guardato), magic_isset_bool, place_isset/
place_empty, prop_value_silent, pack_args. __toString era già 19-6.
- 22-1
__getinread_property,__setinwrite_place(solo[Prop]finale);check_first_prop_writereso magic-aware. 7 test. - 22-2
__isset/__unset;place_isset/place_empty; armPropGetdedicato ineval_isset(silent,??);unset_place→Result. 10 test. - 22-3
__call/__callStatic(trigger: non risolto O non visibile); args in array lista. 6 test. - 22-4
__invokeincall_value+is_callable. 4 test. - 22-5 corpus + 2 fix (vedi D-NEW sotto). 2 test. 29 test totali, 462 suite totale.
Validazione corpus Zend/tests/magic_methods: 19 pass / 21 fail / 117
skip (157 tot). Pass-rate runnable 47.5% (era 42.5% prima dei 2 fix 22-5).
2 fix da corpus (D-NEW):
- D-NEW (bug #44899):
empty($o->p)con__isset→true ma senza__getdeve leggere il valore in silenzio (no warning "Undefined property"). Prima usavoread_property(che avvisa); oraprop_value_silent(chiama__getse c'è, altrimenti valore presente o NULL silenzioso). Esteso anche a??e??=. - D-NEW (bug #53826):
parent::priv()(o metodo ignoto) dentro un metodo ha$this→ instrada a__call(magic d'istanza), non__callStatic. Solo in assenza di$this(chiamata statica pura) si usa__callStatic. Fix incall_static: controllocur_thiscompatibile prima di scegliere il magic.
Fail residui categorizzati (scope-out, feature adiacenti):
__destructnon implementato (4): bug29368_2, bug43175, bug72177, dtor_scope.Stringableauto-interface PHP 8 (3): interface_with_tostring, stringable_automatic_implementation, stringable_trait.- validazione firma/return dei magic method (3): bug26166 (
__toStringdeve ritornare string), magic_methods_008 (__setabstract+private), magic_methods_ 009 (__callStaticnon public/static) — fatal/warning che non emettiamo. &__getby-reference (1): bug70223.- reference dentro prop overloaded
&$o->p(1): bug52879. - differenze formato
var_dump/print_rsu oggetti + edge__get/__setche lancia (≈9): bug32660/36214/37667/38624/39775/48248/72177_2/72813, call_static_006.
Scope-out riepilogo step 22: __destruct, Stringable auto-impl,
validazione firma magic method, &__get by-ref, indirect modification di prop
overloaded multi-livello ($o->magic[] = v), reference a prop overloaded.
PHP 8.1 enum (pure + backed). mago espone Statement::Enum con
Enum { name, backing_type_hint: Option<EnumBackingTypeHint{hint}>, implements, members: Sequence<ClassLikeMember> }. I membri riusano ClassLikeMember:
EnumCase, Method, Constant, TraitUse — quindi il lowering di un enum è
quasi identico a quello di una classe, più la gestione dei case.
EnumCase.item è Unit(name) oppure Backed{name, value: Expression}.
- D-23.1 — Enum riusa
ClassDecl. Comeis_interface, aggiungois_enum: bool,enum_backing: Option<EnumBacking>(Int|Str) eenum_cases: Vec<EnumCaseDecl{ name, value: Option<Expr> }>aClassDecl. Tutta la macchina OOP (resolve_method, instanceof, class const, static call, $this) viene riusata. - D-23.2 — Case = oggetto singleton interned. Cache nell'
Evaluator:enum_cache: HashMap<(ClassId, Vec<u8>), Rc<RefCell<Object>>>. Il primo accesso aE::Casematerializza l'oggetto (props sintetiche), i successivi ritornano lo stessoRc. Garantisce identità per===/match. - D-23.3 — Object identity in
===(FIX gap pre-esistente).ops::identicalnon aveva armObject→ due oggetti erano sempre!==(mai testato finora). Aggiungo(Object(l), Object(r)) => Rc::ptr_eq(l, r): semantica handle PHP corretta (assegnazione oggetto condivide l'Rc) e prerequisito per gli enum. Catalogato come D-NEW in 04-divergences. - D-23.4 — Props sintetiche read-only. Pure:
name(string). Backed:name+value(Long|Str). Accessibili via->name/->value. Sono props normali nell'Object(interned una volta), niente setter. - D-23.5 —
var_dump/print_rformato enum.enum(Suit::Hearts)(anche i backed: niente value nel dump, confermato da oracle). Flagis_enum_case: boolsuObjectInfo;print_r→Suit Enum ( [name] => Hearts [value] => ... )(da validare con oracle in 23-4). - D-23.6 — Metodi built-in.
E::cases()(tutti, lista dei singleton in ordine di dichiarazione);E::from($v)/E::tryFrom($v)(solo backed):from→ ValueError se assente,tryFrom→ null. Special-case incall_staticquandois_enum(prima della risoluzione metodi utente). - D-23.7 — Interfacce prelude
UnitEnum/BackedEnum. Aggiunte vuote al PRELUDE.lower_enumaggiungeUnitEnumagliinterfacesdi ogni enum, eBackedEnumper i backed →instanceof UnitEnum/BackedEnumvia la macchina esistente.implementsutente concatenato. - D-23.8 — Metodi/costanti utente. Lowerati come per le classi; dentro un
metodo
$thisè il case (oggetto).const X = self::Caserisolto via il normale eval delle class-const (i case sono accessibili come const-like). - D-23.9 —
new E()→ Error "Cannot instantiate enum E" ineval_new. - D-23.10 — Backing dal
Hint. Soloint/string. I valori dei case devono essere literal int/string coerenti col backing (validazione lieve); store comeZval::Long/Zval::Str. - D-23.11 —
E::classgià funziona (branchclassineval_class_const).
- Enum che implementano interfacce con metodi reali oltre ai marker (oltre il
semplice
instanceof):implementsregistrato ma non si verifica conformità. - Reflection sugli enum, enum negli attributi,
enum_exists(). - Proprietà dichiarate dentro un enum (PHP: fatal "Enums may not include properties") — le rifiutiamo/ignoriamo, non emettiamo il fatal esatto.
- Costanti enum che referenziano case in contesti complessi oltre
self::Case.
- 23-1 pure enum core: dichiarazione;
Suit::Heartssingleton;->name;===/!==ematchper identità (incl. fix object===inops);instanceof Suit;instanceof UnitEnum;Suit::class;new Suit()→ Error. - 23-2 backed enum:
: string/: int;->value;from/tryFrom;instanceof BackedEnum; ValueError sufrommancante;tryFrom→ null. - 23-3 cases() + metodi/costanti:
Suit::cases(); metodo d'istanza con$this+match($this); metodo statico;constenum;self::Case. - 23-4 var_dump/print_r + corpus: formato
enum(...); print_r; validazione corpusZend/tests/enum*; fix eventuali (D-NEW).
Spedito in 5 commit (23-1 → 23-5). Infra: ClassDecl esteso (is_enum,
enum_backing: Option<EnumBacking{Int,Str}>, enum_cases: Vec<EnumCaseDecl>);
Evaluator.enum_cache: HashMap<(ClassId, Vec<u8>), Rc<RefCell<Object>>> per
l'interning dei case singleton; ObjectInfo.is_enum_case per il rendering.
mago Statement::Enum (membri EnumCase/Method/Constant/TraitUse) viene
lowerato quasi come una classe (lower_enum).
- 23-1 lowering
Statement::Enum(+hoist, +conditional reject);E::Case→ singleton interned ineval_class_const/eval_enum_casecon prop sinteticaname;instanceofviaUnitEnum/BackedEnumnel PRELUDE;new E()→ Error. Fix D-NEW-11: object===(armObjectinops::identical, prima sempre falso). 8 test. - 23-2 backed enum: prop
value;from/tryFromincall_static(riservati, solo backed);ValueErrorcatchable sufrommancante ("X"quotato per string, nudo per int);instanceof BackedEnum. 7 test. - 23-3
E::cases()(lista singleton in ordine); confermato funzionante via macchina OOP riusata: metodi d'istanza con$this=case +match($this), metodi statici, costanti,const = self::Case. 6 test. - 23-4
var_dump→enum(Name::Case);print_r→Name Enum[:int|:string]. Fix D-NEW-12 (loose==oggetti) e D-NEW-13 (costanti d'interfaccia ereditate,find_class_const). 9 test. - 23-5 immutabilità case: prop readonly ("Cannot modify readonly property"),
no dynamic ("Cannot create dynamic property"), no unset ("Cannot unset
readonly property") in
write_into/unset_place. 4 test. 34 test totali, suite 497.
Validazione corpus Zend/tests/enum: 43 pass / 18 fail / 91 skip (152
tot). Pass-rate runnable 70.5% (era 62.3% prima di 23-5).
Scope-out riepilogo step 23: modifica readonly indiretta/by-ref (Cannot indirectly modify…), operatori d'ordine </>/<=/>= fra oggetti, validazioni
compile-time (duplicate backing value, case-type vs backing mismatch, from()
argument TypeError), enforcement degli object type-hint sui parametri,
dipendenze da Reflection*/SplObjectStorage/WeakMap, stack-trace frames. Dettaglio
divergenze D-NEW-11/12/13 in 04-divergences.md.
Generato con assistenza AI (Claude Opus 4.8). Lever data-driven:
fopen(297 file nel corpus) = il bucket builtin #1. Scelta utente 2026-06-21: "spina fopen" (introdurre il tipoResource+ stream su file reali +php://base), sotto-step 51a/51b/51c. Ground truth catturato contro l'oracle 8.5.7.
D-51.1 — nuovo Zval::Resource(Rc<RefCell<Resource>>). Mancava del tutto.
Handle semantics come Object/Generator: il clone condivide l'Rc, quindi
$g = $f aliasa lo stesso stream e fclose($g) chiude anche $f. gettype →
"resource" (aperto) / "resource (closed)" (chiuso). error_type_name →
"resource (closed)"/"resource". Identità $f === $f via Rc::ptr_eq.
D-51.2 — payload in php-types::stream, logica I/O lì, semantica PHP sopra.
Resource { id: u32, kind: ResKind }, ResKind::{ Stream(Stream), Closed },
Stream { backend, readable, writable, eof }, StreamBackend::{ File(fs::File), Memory(Cursor<Vec<u8>>), Stdout, Stderr }. I metodi read/read_line/write/seek/ tell/eof (pura std::io) vivono su Stream; precedente: GenState (payload
runtime) già in php-types. Le regole PHP (mode→capability, false+Warning su
fallimento, EOF flag) stanno nel chiamante (evaluator/builtins). Resource NON
è Clone (sta dietro Rc); File/Cursor sono Debug → derive(Debug) ok.
D-51.3 — fopen evaluator-dispatched, le operazioni builtin puri. fopen
(e gli opener php:// in 51b) servono il contatore id (stato evaluator) →
in HIGHER_ORDER_BUILTINS/dispatch_higher_order come unserialize. fread/ fwrite/fputs/fclose/fgets/fgetc/feof/fseek/ftell/rewind/fflush raggiungono lo
stream tramite l'Rc<RefCell<Resource>> dell'argomento → builtin puri in
php-builtins/src/file.rs, nessuno stato evaluator necessario.
D-51.4 — base id risorse = 5. Oracle CLI -n: STDIN/STDOUT/STDERR = id 1/2/3,
id 4 interno, primo fopen utente = id 5, poi monotono. next_resource_id
inizializzato a 5. STDIN/STDOUT/STDERR non modellati come costanti in Tier 1
(se serviranno, id 1/2/3 fissi). L'id esatto conta solo per i test EXPECT
(esatti); gli EXPECTF usano %d → divergenza-prone, documentata.
D-51.5 — formati osservabili (oracle-verified):
var_dump($f)aperto →resource(N) of type (stream); chiuso →resource(N) of type (Unknown).print_r/echo/(string)$f→Resource id #N.(int)$f→N(l'id);(bool)$f→true;(float)$f→N as f64.serialize($f)→i:0;(PHP serializza le risorse come intero 0).- aritmetica/
try_to_numbersu risorsa → l'id (niente Warning in Tier 1). feofriflette l'EOF flag: falso finché una lettura non oltrepassa la fine (verificato: dopo aver letto esattamente fino a EOF è ancora false).
D-51.6 — enforcement mode su file reali. r=read, w=write+truncate,
a=write+append, x=create-excl+write, c=write-no-truncate; suffisso +
aggiunge l'altra capability; b/t ignorati. fwrite su stream non-writable e
fread su non-readable → false + Warning (oracle: bool(false)). php://memory
è lenient sul mode (oracle accetta "zz"); per i file reali un mode senza
r/w/a/x/c → false.
- 51a:
Zval::Resource+php-types::stream+next_resource_id+fopen(solo file reali) +fread/fwrite/fputs/fclose. ArmResourcein tutti i match esaustivi suZval(convert/ops/var_dump/print_r/serialize). - 51b:
fgets/fgetc/feof/fseek/ftell/rewind/fflush+ openerphp://memory/php://temp/php://stdout/php://stderr. - 51c:
file_get_contents/file_put_contents(builtin puri, no Resource).
Wrapper http:///https:///ftp:///data:///zlib:///phar://, stream
context (stream_context_create), stream filter (stream_filter_append,
php://filter), socket stream (fsockopen, stream_socket_*),
stream_get_contents/stream_get_line/stream_select, php://input,
registrazione wrapper custom (stream_wrapper_register), predicati FS
(file_exists/is_file/mkdir/unlink/rename/copy/glob/scandir) —
candidati a uno step FS dedicato dopo 51.
| ID | Costrutto C/PHP | Scelta Rust | Razionale | Status |
|---|---|---|---|---|
| D-52.1 | basename/dirname/pathinfo |
byte-op puri su &[u8], no I/O |
path PHP sono byte; nessuno stat necessario | confermato |
| D-52.2 | file_exists/is_file/is_dir |
std::fs::metadata (segue symlink) |
un symlink rotto → false (oracle) |
confermato |
| D-52.3 | is_link/filetype |
symlink_metadata (no-follow) |
symlink rotto resta true; type da lstat |
confermato |
| D-52.7 | is_readable/is_writable/is_executable |
libc::access(2) (R/W/X_OK) |
euid-aware: chmod 0 → non leggibile anche per l'owner; il vecchio metadata().readonly() non distingueva |
confermato |
| D-52.8 | clearstatcache |
no-op null |
non teniamo cache di stat per-richiesta (ogni call colpisce l'FS fresco) → niente da invalidare; semantica più corretta di PHP-C | confermato |
| D-52.9 | array stat/lstat/fstat |
26 voci: int 0..=12 poi nominali, da MetadataExt |
ordine osservabile per var_dump/accesso; valori i64 (dev/ino/size stanno) | confermato |
| D-52.10 | fstat su stream non-File |
array sintetico mode 0100666 + size buffer | stream in-memory non hanno inode; l'oracle dà 33206 per php://memory |
confermato |
| D-52.11 | glob |
globber shell self-contained (no crate) | controllo esatto su */?/[...], dot-rule, GLOB_BRACE, flag; il crate glob ha semantica propria divergente da PHP |
confermato |
| D-52.12 | opendir/readdir/closedir |
scope-out | scandir copre la forma comune; iterazione resource-based differita |
scope-out |
| D-52.13 | tmpfile |
evaluator-dispatched (conia resource); file unlinkato r+ | possiede next_resource_id come fopen/tmpfile; unlink-while-open = auto-cleanup POSIX |
confermato |
| D-52.14 | tempnam |
crea file 0600, ritorna path canonicalizzato | l'oracle risolve /var→/private/var su macOS via realpath |
confermato |
| ID | Costrutto C/PHP | Scelta Rust | Razionale | Status |
|---|---|---|---|---|
| D-53.1 | dir handle in un builtin di stream | stream_arg ammette solo ResKind::Stream(_); fstat ritorna false su dir/closed |
un ResKind::Dir raggiungeva as_stream_mut().expect() → panic (corpus directory_wrapper_fstat_basic); il rigetto mantiene sani gli 8 .expect() |
confermato |
| D-53.2 | opendir/readdir/closedir/rewinddir |
ResKind::Dir(DirHandle) (snapshot voci + cursore); opendir evaluator-dispatched |
PHP modella i dir handle come php_stream (stesse etichette "resource"/"stream"); readdir ordine OS con ./.. in testa |
confermato |
| D-53.3 | fprintf/vfprintf |
riuso di format_impl/first_format (sprintf engine) → stream |
zero duplicazione dell'engine di formattazione; ritorna il byte-count come printf |
confermato |
| D-53.4 | strrchr |
usa solo il primo byte del needle, ultima occorrenza | semantica storica PHP (oracle-verified) | confermato |
| ID | Costrutto PHP | Scelta Rust | Razionale | Status |
|---|---|---|---|---|
| D-54.1 | sscanf/fscanf by-ref out-var |
higher-order ho_sscanf/ho_fscanf + write_out_param |
l'ABI builtin puro non assegna a &$var; solo $var bare (array-elem/prop ignorati, come preg_match) |
confermato |
| D-54.2 | CSV $escape omesso |
emette Deprecated "$escape parameter must be provided…" |
i .phpt 8.5 lo hanno in EXPECTF; rilevato via argv.len() |
confermato |
| D-54.3 | record CSV multi-riga in fgetcsv |
leggiamo una sola riga (scope-out) | continuazione su più righe di un campo quotato è rara; str_getcsv gestisce \n embedded |
scope-out |
| D-54.4 | motore scanf | nuovo php-runtime/src/scanf.rs (no php-builtins) |
ho_sscanf vive nell'evaluator (by-ref) e php-runtime non dipende da php-builtins |
confermato |
| D-54.5 | motore CSV | nuovo php-builtins/src/csv.rs condiviso |
funzioni CSV pure; primo-byte di sep/enc/esc, escape vuoto = disabilitato | confermato |
| D-54.6 | %i vs %d |
%i auto-base (0x/0), %d decimale stretto |
semantica C/PHP (oracle: %i di "0x1A"→26, di "ff"→NULL) |
confermato |
| ID | Costrutto PHP | Scelta Rust | Razionale | Status |
|---|---|---|---|---|
| D-55.1 | file() line-split |
split su \n, newline tenuto/strippato per flag |
default tiene \n; IGNORE_NEW_LINES strippa \r?\n; SKIP_EMPTY scarta righe vuote (oracle) |
confermato |
| D-55.2 | ftruncate |
match StreamBackend: File::set_len / Memory Vec resize(0-fill) |
i due backend troncano in modi diversi; Stdout/Stderr→false | confermato |
| D-55.3 | stream_copy_to_stream |
read-all in buffer poi write | evita doppio borrow di from/to (anche se stesso resource) |
confermato |
| D-55.4 | disk_*_space |
libc::statvfs (f_bavail/f_blocks * f_frsize) → f64 |
nessun equivalente in std; libc già dep; Unix-only | confermato |
| D-55.5 | putenv |
std::env::set_var/remove_var (edition 2021, safe) |
process-global; ok sotto --isolate (un processo per test) |
confermato |
| D-55.6 | named-args ai builtin | scope-out (refactor ABI + tabella ~199 fn) | ROI basso vs il costo; ribadito da D-38.2 | scope-out |
| ID | Costrutto PHP | Scelta Rust | Razionale | Status |
|---|---|---|---|---|
| D-56.1 | htmlentities tabella entità |
solo Latin-1 (U+00A0–U+00FF) via decode UTF-8 | il set HTML4 completo (greco/matematica) è grande e raro; Latin-1 copre il caso comune (café→é) | scope-out parziale |
| D-56.2 | htmlspecialchars flag default |
11 (ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); bit1=', bit2=" |
| D-56.3 | wordwrap |
greedy con replace dello spazio (multi-char break = rebuild) | port fedele dell'algoritmo PHP; verificato sui casi wrap dell'oracle | confermato |
| D-56.4 | vsprintf/vprintf |
riuso format_impl (slot 0 = formato dummy) |
zero duplicazione dell'engine, come vfprintf |
confermato |
| D-56.5 | substr_replace |
solo forma scalare | la forma-array (string/replace come array) è rara; scalare copre il grosso | scope-out parziale |